Skip to main content
A config blob is one JSON file holding all or part of an organization’s configuration: teams, roles and people, connectors, AI Gateway providers and limits, plugins and skills, desktop policies, Workbot, and settings (26 areas, listed below). You can export it, keep it in version control, edit it, and import it again into the same organization or into another OpenWork (for example from OpenWork Cloud to a self-hosted Den). Nothing is written when you upload a file. Every import becomes a change request: OpenWork matches the file against what exists, shows a plan, and only writes after someone approves it. An applied change can be undone with Restore previous config, which puts the configuration back (see Restore previous config for what it can’t undo).
Config import and export is behind the configBlobs feature, off by default. On a self-hosted Den, a platform administrator turns it on per organization from /admin, or for the whole installation with config.features.configBlobs: "true" in Helm (DEN_FEATURE_CONFIG_BLOBS=true). While it is off, every /v1/config-blobs/* route except the public JSON Schema answers 404.
This page complements Provision organization configuration. The keyed PUT …/by-key/{key} routes stay supported and are the simplest choice when a script owns a handful of resources. Use config blobs to copy a whole organization, to review configuration changes as a diff, or to move between deployments.

The file

  • scope lists the areas the file owns. Only those areas are compared and changed. An area in scope with "items": [] means “this area should be empty”, which only deletes anything in mirror mode.
  • areas nests area keys on .: connectors.mcp is areas.connectors.mcp.
  • Each item has an id (the source ID), an optional key (the resource’s external key), a name, a rev (hash of the spec at export, used to detect changes made on the target since) and a spec, the editable part. Unknown fields are rejected, so typos never get dropped silently. You may remove id and rev from hand-written items; they are then matched by key or name.
  • References to other items are { "$ref": "members.teams/tem_…", "name": "Platform" }. People are referenced by email: { "$ref": "people/ada@example.com" }.
  • Files are limited to 5 MB.
The JSON Schema for editors and CI is public, needs no authentication and is not behind the feature flag:
Exports set $schema to this URL on the server that produced them (from DEN_API_PUBLIC_URL), so editors that understand $schema validate and complete the file.

Areas

Importing an area (uploading, or a dry run) needs both permissions in its row: a plan shows the area’s current settings next to the file’s, so someone who may change an area but not view it can’t import it. Some areas carry only part of what their screen shows:
  • People are invited, never added directly: someone who is a member on the source gets a pending invitation (silent unless you choose to send emails). An owner is invited as an admin.
  • Sources need the GitHub App installed in the target organization first: an installation belongs to the server and organization that made it.
  • Auth & Security: a new SSO connection starts disabled and unverified (verify the domain, test, then enable it); SCIM tokens are never carried.
  • Skills that a source syncs are not imported (the source brings them), and the Workbot “Your agent” template travels with workbot.proactive.
GET /v1/config-blobs/areas lists the areas of your server, whether each is supported there, and whether you may export or import it. An export leaves out the areas you can’t view and lists them in omitted. Never exported: credentials and tokens, billing, usage and runtime data (sessions, logs, audit events). The organization’s name, slug and SSO domains are exported as source information only and are never applied. Some areas contain confidential content; treat an export like the configuration it describes.

Secrets

Secrets are never in a file. Every secret field is a placeholder:
set says whether the source had a value. The placeholder path is stable, so a file you edit and upload again keeps its placeholders. Before a change that writes a secret can be approved, each placeholder needs a decision, given in the file or through the inputs endpoint:
  • Values are write-only: stored encrypted on the change request, never returned, used once at apply and then deleted. They are also deleted when the request is cancelled, superseded or invalid, and when it is not applied within 7 days (the request then asks for them again). Entering a value drops existing approvals.
  • Environment variables are self-hosted only, and only for names the operator allows in DEN_CONFIG_BLOB_SECRET_ENV_ALLOWLIST on the den-api process (Helm: denApi.env). It is a comma-separated list of exact names, or prefixes ending in *. Only names starting with OPENWORK_IMPORT_ can be read, so every entry must start with it:
    Other entries (a bare *, D*, OPENWORK_*, DATABASE_URL…) are ignored, with a warning in the den-api log at startup, and Den’s own settings (DATABASE_*, DEN_*, BETTER_AUTH_* and other credentials it reads) are refused even if listed. The variable is read when the change is applied; if it is missing, nothing is applied. OpenWork Cloud never reads environment variables for an organization.

Change requests

Uploading returns 202 with a change request ID. The server analyzes it in the background.
  • Modes: merge (default) creates and updates only. mirror can also delete items of the scoped areas that are not in the file; each delete must be ticked in the decisions.
  • Matching: an item matches by ID, then by an alias from an earlier import, then by key, then by a natural key that is unique on both sides (for example an MCP URL). A name alone only proposes a match that you confirm.
  • Plans: each item is create, update, rename, unchanged, delete, untouched or skip, with field-level changes. Items blocked by a missing decision are counted in plan.summary.blocking; approval is refused until it is 0.
  • Checks before approval: the plan already checks what would fail on this organization. Items that can’t apply here are skipped with a reason (for example a skill a source syncs); items that need something first are blocked with what to do (connect the GitHub App, allow an email domain, set up SSO before requiring it, enter a key again for a provider that moves to another address). Fix it and change any decision to re-check, or skip the item.
  • Approvals are tied to planHash. Any change to the plan or a secret value drops them. An organization can require a second approver (Settings › Config), who must differ from the uploader and the first approver.
  • Approving and applying in the dashboard ask you to sign in again when your sign-in is older than your organization’s fresh-session window. A config token with config:apply approves without that. A change is applied within 24 hours of its last approval; after that it goes back to review and needs approving again.
  • Apply runs as the last approver: they (or the config token’s member) must still hold every written area’s permission. Right before writing, the server re-reads the items it is about to write; if any changed since the plan, the request goes stale. Each item is written on its own, with only the changes the plan lists; there is no single transaction, so the apply stops at the first failing item and its report says what was written. When a request is applied, the organization’s other open requests go stale.
  • IDs: a created item from another server reuses its source ID when that ID is free on the whole server and was never used in this organization before, so cloud → self-hosted → cloud keeps the same IDs. Otherwise, and always for a file exported from this same server, it gets a new ID and an alias, so the next import of the same file matches it (“matched via an earlier import”).
  • Interrupted applies are never retried: the request fails, and you can restore the previous config from its before-snapshot.

Restore previous config

Every apply first saves a before-snapshot of the items it is about to write. Restore previous config (API: POST …/revert) creates a new change request from it, reviewed and applied like any other: items it created are deleted, items it changed are changed back, and changes made since show up as conflicts. It restores configuration only. It can’t undo what the apply set off: invitation emails sent, sign-ins revoked or ended, credentials replaced or deleted (snapshots never hold secrets: a replaced key keeps its new value, and a deleted item’s keys must be entered again), members removed (they can only be invited again), syncs a source ran, and history (skill versions, audit events). Recreated items get new IDs. The plan lists these effects on each item before you approve, and the restore’s plan repeats them. Every apply is one audit operation (config_blobs.apply) with the changes of each item.

Config tokens

Automation uses a config token: an organization API key of type config. Create one in Settings › API keys, or with the API (needs the Manage API keys permission):
  • Scopes are independent: config:apply does not include config:submit.
  • areas (optional) limits the token to area keys or prefixes such as ai-gateway.*. A limited token can’t export, upload or read other areas.
  • A config token only works with the Config API (/v1/config-blobs/*). Every other route, including the MCP endpoints, answers 403 config_token_scope; use a standard API key for those.
  • A config token acts as the member who created it and can never do more than that member could in the dashboard.
  • Only config tokens with config:submit may send secret values. Standard API keys get 403 config_scope_required on every config-blob route.
  • A new upload from a token supersedes that token’s older open requests with exactly the same scope.

API

All routes take the token in x-api-key; the organization comes from the token. Export, upload, wait for analysis and read the plan:
Answer the secret placeholders, then approve and apply with a token that holds config:apply:
Poll the request until it is applied or failed; the response’s report lists each item.

Move from OpenWork Cloud to a self-hosted Den

  1. On Cloud, turn on config import and export for the organization and export everything (Settings › Config › Export, or GET /v1/config-blobs/export).
  2. On the self-hosted Den, create the organization and sign in as its owner. Turn on configBlobs. If you want to supply secrets from the environment, set DEN_CONFIG_BLOB_SECRET_ENV_ALLOWLIST and the variables on den-api.
  3. Upload the file in Settings › Config › Import (or POST …/change-requests), in merge mode.
  4. Review the plan. Everything is a create; source IDs are reused. People become invitations; choose whether to send invitation emails now (decisions.people.sendInviteEmails) or later. Resolve any references the plan flags.
  5. Secrets: for each placeholder enter a value, choose an environment variable, or leave it unset and finish setup later on the resource’s page.
  6. Approve and apply, then export again: the IDs and specs match the Cloud export. From now on, importing the next Cloud export shows only what changed.
Cloud-only features don’t exist on self-hosted servers; their areas are reported as not supported and left out.

CI with GitHub Actions

Keep the export in a repository. A schedule records drift as a pull request, pull requests are checked against the public JSON Schema, and merges to main submit a change request that an admin approves in the dashboard (or that a token with config:apply approves).
Never give the config token to a job that a pull request triggers. A pull request can change the workflow file or the code a job runs, and so read any secret that job can read. Only the scheduled export and the submit after a merge hold the token; the pull request check needs none, because the schema is public.
Store the token as a secret of a GitHub environment (here openwork-config) that only main may deploy to, protect main with required reviews, and add required reviewers to the environment if you want a second person to release each run. The token reaches the shell as an environment variable, never written into the command in the workflow file. The trusted workflow, .github/workflows/openwork-config.yml:
The pull request check, .github/workflows/openwork-config-check.yml, has no secrets: it validates the file against the schema your server publishes.
The full plan (what each item will do, secrets to fill in, blocking decisions) is produced after the merge, when the submitted change request is analyzed, and an admin reviews it in Settings › Config before anything is applied. If you also want a dry run (?dryRun=true) on pull requests, run it only from a trusted workflow that checks out the base branch, reads the pull request’s file as data (through the GitHub API, never by running anything from the pull request) and keeps its output private: a plan shows the organization’s current configuration next to the file’s. Give the CI token config:export and config:submit only, so a person approves every change, and limit it to the areas the repository manages. A dry run reports errors (what would make the request invalid) and plan.summary.blocking (items that need a decision, such as secrets); it stores nothing and never supersedes an open request. Each push supersedes the previous open request from the same token for the same scope. Export exportedAt changes on every run; drop it before committing, as above, so a scheduled export only opens a pull request when configuration really changed.