Export and Import an Instance
Polyant can serialise an entire instance into a JSON bundle and re-create it elsewhere — useful for backup, promoting from staging to production, or sharing a template between teams.
The bundle is safe to share over normal channels: secret VALUES are never included. Only the list of which secret keys are configured travels with the export. Channel and skill configuration travels with every credential-like key removed — recursively, so a credential nested inside a structured sub-config does not leak either.
This guide covers the full round trip and the manual steps you must run after import.
Prerequisites
- An authenticated admin session (cookie or Bearer token).
- The
slugof the source instance (e.g.my-assistant). - For “overwrite mode”, the destination instance must already exist.
Step 1 — Export the bundle
curl -s http://localhost:4000/api/instances/my-assistant/export \
-H "Cookie: authjs.session-token=$TOKEN" \
-o my-assistant.bundle.jsonThe export endpoint returns indented JSON with a Content-Disposition header so browsers download it as a file. curl -o works fine; piping through jq also works.
What the bundle contains (assembled in export.service.ts):
- Instance metadata —
slug,name,description,status,provider,model,icon,sttProvider,langsmithProject, plus the togglesmemoryEnabled,knowledgeEnabled,langsmithEnabled,authEnabled. - Behaviour configuration —
thinkingEnabled,temperature,stateInPromptEnabled,datetimeInjectionEnabled,cacheEnabled,cacheTtl,toolResultsInHistoryEnabled,debugEnabled,governanceHistoryTurns, and the embedder (embeddingProvider,embeddingDim). - Opt-out configuration — enabled flag, stop/resume keywords, closing and resume messages, prompt-hint flag. See Privacy.
- Prompts — every section, full text.
- Skills — assignments only (slug + enabled + autoLoad + pinnedVersion). The skill DEFINITIONS live in the global catalog; if the destination engine lacks a skill, the import will warn.
- Manual tools — the list of tool names that were explicitly added on top of the skill-derived defaults.
- Secrets —
[{ key, configured }]. Values are stripped. - Channels —
[{ channelType, enabled, config }], whereconfigholds the non-secret settings only (allowedUserIds,whatsappNumber, an HTTP channel’s field mapping, …). Credential-like keys are removed recursively, including from nested sub-configs. - Skill env —
[{ skillSlug, key, encrypted, value? }]. Encrypted values are stripped; only non-encrypted ones travel. - Hooks — event, function, enabled, position, timeout for each lifecycle hook.
- Room config — prompt, outbound channel/target, eval interval.
- Event sources — with their definitions (matching prompt, interpretation prompt, routing).
- Scheduled tasks — schedule, prompt, outbound channel/target, retention flags.
What the bundle does NOT contain:
- Conversation history, conversation state, memories, audit logs, pipeline traces — operational data, not configuration.
- Any secret value, and therefore no channel credential.
- The knowledge base. It has its own endpoints (
GET /api/instances/:slug/knowledge/exportandPOST .../knowledge/import), so moving it is a deliberate, separate step rather than something that rides along with a config bundle. - The retention policy and the opt-out contact list (the configuration travels; the per-contact statuses do not).
The bundle format is versioned. The exporter emits 1.1; 1.0 bundles still import, with the fields added after 1.0 taking their defaults.
Step 2a — Import as a new instance
This is the safest mode. The bundle is parsed through instanceBundleSchema and the slug is taken from the bundle itself; if it collides with an existing instance, the engine auto-resolves to a unique slug (e.g. my-assistant-2, my-assistant-3, …).
curl -s -X POST http://localhost:4000/api/instances/import \
-H "Content-Type: application/json" \
-H "Cookie: authjs.session-token=$TOKEN" \
--data-binary @my-assistant.bundle.jsonIf you want to control the destination slug, edit the bundle’s instance.slug field before posting (the slug lives under instance, not at the top level — the bundle’s top-level keys are version, exportedAt, type, instance):
jq '.instance.slug = "my-assistant-staging"' my-assistant.bundle.json > staging.bundle.json
curl -s -X POST http://localhost:4000/api/instances/import \
-H "Content-Type: application/json" \
--data-binary @staging.bundle.jsonThe response includes the created instance and a warnings array. Each warning is an object with type and message; the types are secret_required, missing_skill, missing_tool, channel_credentials, skill_env_required, and event_source_credentials:
{
"warnings": [
{ "type": "secret_required", "message": "Secret \"openai_api_key\" needs to be configured" },
{ "type": "missing_skill", "message": "Skill 'foo' not found in catalog — assignment skipped" },
{ "type": "missing_tool", "message": "Manual tool 'bar' not registered on this engine" },
{ "type": "channel_credentials", "message": "Channel \"telegram\" imported disabled — configure credentials to enable" }
]
}Read the warnings before testing: an agent imported with a disabled channel and unconfigured secrets looks broken for entirely expected reasons.
Step 2b — Import as overwrite
Use this to update an existing instance in-place. The target slug is taken from the URL.
curl -s -X POST http://localhost:4000/api/instances/my-assistant/import \
-H "Content-Type: application/json" \
-H "Cookie: authjs.session-token=$TOKEN" \
--data-binary @my-assistant.bundle.jsonOverwrite replaces prompts, skill assignments, manual tools, hooks, room config, event sources, and scheduled tasks. Existing instance secrets and channel credentials are preserved — the bundle has nothing to overwrite them with anyway.
One field is deliberately not applied on overwrite: the embedder. Changing it on a live agent wipes its memories and knowledge base, so an overwrite import leaves embeddingProvider and embeddingDim untouched. They are applied on import-as-new only. To change an existing agent’s embedder, do it explicitly on the Settings tab and accept the wipe.
Step 3 — Re-enter secrets
Because the bundle ships only the list of configured keys, you must add each value back on the destination engine:
curl -s -X PUT http://localhost:4000/api/instances/my-assistant-staging/secrets \
-H "Content-Type: application/json" \
-d '{
"secrets": [
{ "key": "openai_api_key", "value": "sk-..." },
{ "key": "langsmith_api_key", "value": "ls_..." }
]
}'See Manage secrets for the full reference.
Step 4 — Reconnect channels
Channel rows are created on import, carrying the non-secret configuration so you only have to fill in what was stripped. But a channel is re-enabled only if its non-secret config alone satisfies the channel’s validation schema — true today only for the credential-less agent channel. Telegram, Slack, WhatsApp, and a credentialed HTTP outbound therefore arrive disabled, each with a channel_credentials warning.
Add the missing credentials from the channel’s card in the admin panel, or via the channel endpoint:
curl -s -X PUT http://localhost:4000/api/instances/my-assistant-staging/channels/telegram \
-H "Content-Type: application/json" \
-d '{ "config": { "botToken": "12345:ABC..." }, "enabled": true }'See Connect a channel for the per-channel payload.
Bonus — Move the knowledge base
The knowledge base is not part of the instance bundle. Move it explicitly, after the instance exists and its embedder is set:
curl -s http://localhost:4000/api/instances/my-assistant/knowledge/export -o knowledge.bundle.json
curl -s -X POST http://localhost:4000/api/instances/my-assistant-staging/knowledge/import \
-H "Content-Type: application/json" \
--data-binary @knowledge.bundle.jsonImport re-embeds every document with the destination agent’s current embedder, so the two agents do not need the same one. The destination must have knowledgeEnabled on and its embedder credentials configured, or the request is rejected before anything is written. The whole bundle is validated up front — the import is all-or-nothing — and a filename collision is resolved by appending a suffix (manuale.txt → manuale (1).txt), never by overwriting.
Both endpoints are rate-limited to 5 requests/minute and require agent.knowledge:read / agent.knowledge:write.
Bonus — Export and import the global skill catalog
The catalog is independent of any instance. Use these endpoints before importing instances that depend on custom skills (note the catalog/ segment — easy to miss):
# Export
curl -s http://localhost:4000/api/skills/catalog/export -o skills.bundle.json
# Import
curl -s -X POST http://localhost:4000/api/skills/catalog/import \
-H "Content-Type: application/json" \
--data-binary @skills.bundle.jsonVerification
After Step 2, before Steps 3–4:
curl -s http://localhost:4000/api/instances/my-assistant-staging/secrets | jq '.secrets[] | select(.configured == false)'- All non-default secret keys appear with
configured: false— they need values. GET /api/instances/my-assistant-staging/channelslists the imported channels with their non-secret config, credentialed onesenabled: false— add credentials to enable them.- The prompts, skill assignments, hooks, room config, event sources, and scheduled tasks match the source — the configuration round-tripped cleanly.
- The knowledge base is empty unless you moved it with the knowledge export/import endpoints.
Once Steps 3 and 4 are done, a test message via the Playground (or POST /v1/chat/completions) should succeed end-to-end.