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. The same is true for skill env vars and channel credentials.
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,thinkingEnabled. - Prompts — all 8 sections, 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: true }]. Values are stripped. - Channels —
[{ channelType, enabled }]. Credentials are stripped. - Skill env —
[{ skillSlug, key, encrypted, hasValue }]. Values are stripped. - Room config — prompt, outbound channel/target, eval interval.
- Event sources — with their definitions (matching prompt + interpretation prompt).
- Scheduled tasks — schedule, prompt, outbound channel/target, retention flags.
What the bundle does NOT contain: conversation history, memories, audit logs, pipeline traces — these are operational data, not configuration.
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:
{
"warnings": [
{ "type": "secret_required", "message": "Secret \"openai_api_key\" needs to be configured" },
{ "type": "skill_not_in_catalog", "message": "Skill 'foo' not found in catalog — assignment skipped" },
{ "type": "tool_not_registered", "message": "Manual tool 'bar' not registered on this engine" }
]
}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, room config, event sources, and scheduled tasks. Existing instance secrets and channel credentials are preserved — the bundle has nothing to overwrite them with anyway.
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
No channel rows are created on import — GET /api/instances/:slug/channels on a freshly imported instance returns {"channels": []}. You must PUT each channel manually. Open each channel tab in the admin panel and paste the tokens, or use the channel endpoints:
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 — 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/channelsreturns{"channels": []}— channels must be configured manually viaPUT.- The prompts, skill assignments, room config, event sources, and scheduled tasks match the source — the configuration round-tripped cleanly.
Once Steps 3 and 4 are done, a test message via the Playground (or POST /v1/chat/completions) should succeed end-to-end.