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.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
scopelists the areas the file owns. Only those areas are compared and changed. An area inscopewith"items": []means “this area should be empty”, which only deletes anything inmirrormode.areasnests area keys on.:connectors.mcpisareas.connectors.mcp.- Each item has an
id(the source ID), an optionalkey(the resource’s external key), aname, arev(hash of the spec at export, used to detect changes made on the target since) and aspec, the editable part. Unknown fields are rejected, so typos never get dropped silently. You may removeidandrevfrom 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.
$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_ALLOWLISTon the den-api process (Helm:denApi.env). It is a comma-separated list of exact names, or prefixes ending in*. Only names starting withOPENWORK_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 returns202 with a change request ID. The server analyzes it in the
background.
- Modes:
merge(default) creates and updates only.mirrorcan 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,untouchedorskip, with field-level changes. Items blocked by a missing decision are counted inplan.summary.blocking; approval is refused until it is0. - 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:applyapproves 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 gostale. - 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 typeconfig. Create
one in Settings › API keys, or with the API (needs the Manage API keys permission):
- Scopes are independent:
config:applydoes not includeconfig:submit. areas(optional) limits the token to area keys or prefixes such asai-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, answers403 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:submitmay send secret values. Standard API keys get403 config_scope_requiredon 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 inx-api-key; the organization comes from the token.
Export, upload, wait for analysis and read the plan:
config:apply:
applied or failed; the response’s report lists
each item.
Move from OpenWork Cloud to a self-hosted Den
- On Cloud, turn on config import and export for the organization and export
everything (Settings › Config › Export, or
GET /v1/config-blobs/export). - 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, setDEN_CONFIG_BLOB_SECRET_ENV_ALLOWLISTand the variables on den-api. - Upload the file in Settings › Config › Import (or
POST …/change-requests), inmergemode. - 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. - Secrets: for each placeholder enter a value, choose an environment variable, or leave it unset and finish setup later on the resource’s page.
- 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.
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 tomain
submit a change request that an admin approves in the dashboard (or that a token
with config:apply approves).
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:
.github/workflows/openwork-config-check.yml, has no
secrets: it validates the file against the schema your server publishes.
?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.