Build a bundle file
A workflow, an agent or a grid template moves between workspaces as one zip, a bundle. Export on the workflow or agent page, or on a grid template's row, writes it; the Import button beside it reads it. Inside, nothing identifies the workspace it came from: an agent is named by a key the file invents (agent-1, agent-2), a workflow's steps refer to those keys, and no member, role or record id travels. So the file can be written by hand, or by an AI assistant, as readily as it can be exported.
Everything an import creates arrives as a draft, under the importing person's name, and nothing is used until it is promoted. A drafting library — a document type with its fields, blocks, templates and tests — has its own file, described in Build a library import file.
Before you start
- Automation plan for workflows and agents (the Workflows and Agents pages); grid templates need Analysis grids.
- Importing a workflow needs the workflow-author permission, importing an agent the agent-author permission; a grid template can be imported by any member.
- A bundle is at most 250 MB. Each JSON entry inside it is at most 1 MB, and a bundle holds at most 2,000 entries.
What's in the file
A zip with manifest.json at the top and the entries for one kind of thing beside it. Every JSON file is UTF-8 with camel-case property names.
manifest.json the manifest — what the bundle is
workflow/workflow.json a workflow bundle: the workflow's name and description
workflow/workflow.yaml its steps, in the workflow language
workflow/diagram.json the layout of the visual editor (optional)
agents/agent-1/agent.json an agent — the workflow's first, or the bundle's only one
agents/agent-1/documents/doc-1/document.json one of its knowledge documents
agents/agent-1/documents/doc-1/binary that document's bytes
template/template.json a grid-template bundle: the questions and the model
A workflow bundle carries workflow/… and one agents/… folder for every agent its steps use. An agent bundle carries one agents/… folder and nothing under workflow/. A grid-template bundle carries template/template.json alone. Agent keys run agent-1, agent-2, … in the order the file lists them; document keys run doc-1, doc-2, … within each agent.
The manifest
manifest.json says what the bundle is. Nothing in it identifies the workspace the file came from.
| Property | Value |
|---|---|
formatVersion |
1 — the file format. An import refuses a file written for a newer format rather than guess. |
kind |
workflow, agent or grid-template. |
name |
The name the import shows — the workflow's, the agent's or the template's. |
exportedAtUtc |
When the file was written, as an ISO date and time in UTC. |
entityNames |
The names of everything the bundle creates — the workflow and its agents, say. |
notices |
Notes shown to the person importing: a reviewer list that was reduced to admins, a knowledge document that was skipped. |
A workflow
workflow/workflow.json holds the name and an optional description. workflow/workflow.yaml holds the steps, written exactly as the workflow editor's YAML tab shows them — the workflow reference is the language — with two differences a bundle makes:
- Every agent step's
agentIdis the agent's bundle key (agent-1), never an id from a workspace, and itsagentVersionisactive. The import creates the agents first and rewrites the keys to the new agents' ids. - A review step's
reviewerslist carries onlyadminsandowner. A bundle names no member and no role; a list that would be empty is written asadmins, and the manifest says so, so the reviewers are chosen again after the import.
workflow/diagram.json, when present, is the visual editor's layout for the steps; without it the editor lays the workflow out itself.
An import validates the rewritten YAML as the editor would and refuses the whole bundle, creating nothing, when it does not pass.
An agent
agents/<key>/agent.json:
| Property | Value |
|---|---|
key |
The bundle key — the folder's name, agent-1. |
name |
The agent's name. The import adds a suffix when the workspace already has one by that name. |
description |
Optional. |
systemPrompt |
The system prompt, as the agent editor shows it. Optional. |
greetingMessage |
The first message a conversation with the agent shows. Optional. |
usageContext |
chat for an agent people talk to, workflow for one that works inside workflows. An agent inside a workflow bundle is imported as a workflow agent whatever this says. |
documents |
The knowledge documents, one entry per document, each { key, filename, mimeType }. |
No model is named: the workspace assigns its models when the agent is created, as it does for an agent made in the editor.
Each knowledge document has a folder agents/<key>/documents/<docKey>/ holding document.json — the same { key, filename, mimeType } as the agent's list — and binary, the file's bytes under that fixed name. A document's mimeType is one the workspace accepts for an upload: application/pdf, a Word document, Markdown or plain text. The import stores each document and processes it as an upload, within the workspace's document size and page limits; a document that fails them stops the import.
A grid template
template/template.json:
| Property | Value |
|---|---|
name |
The template's name. |
description |
Optional. |
modelId |
The model the grid's questions run on. When the workspace does not serve it, the import uses the workspace's default and says so in a notice. |
columns |
The questions: each { label, prompt, shape, order }, with shape one of text, yesNo, date or number. |
A grid template carries questions and a model, never documents or answers.
What the import does
- Reads the manifest, and refuses a file with no manifest, an unknown
kind, or aformatVersionnewer than the workspace's. - Checks what the bundle would create against the workspace's limits — agents against the agent limits, knowledge documents against the storage limit — before creating anything.
- Creates every agent as a draft, then every knowledge document, then the workflow as a draft with its YAML rewritten onto the new agents, or the grid template. A name the workspace already has gets a suffix.
- Shows the notices: the exporter's, and its own — renamed items, the reduced reviewer lists, a model it replaced.
Anything that fails after the first write undoes everything the import created, so a workspace never holds half a bundle.
A complete example
A workflow with one agent and no knowledge documents. Save the four files under the names shown, zip them and import the zip on the Workflows page.
manifest.json:
{
"formatVersion": 1,
"kind": "workflow",
"name": "Letter review (example)",
"exportedAtUtc": "2026-10-18T09:00:00Z",
"entityNames": ["Letter review (example)", "Letter reviewer (example)"],
"notices": []
}
workflow/workflow.json:
{
"name": "Letter review (example)",
"description": "Reviews a letter and reports what to change. An example for learning bundles, not for real work."
}
workflow/workflow.yaml:
name: Letter review (example)
version: '1'
entry: review
steps:
- id: review
kind: agent
agentId: agent-1
agentVersion: active
agents/agent-1/agent.json:
{
"key": "agent-1",
"name": "Letter reviewer (example)",
"description": "Reads a letter and lists what to change before it is sent.",
"systemPrompt": "You review business letters. List, as numbered points, anything that is unclear, missing or wrongly worded, and suggest the change. Do not rewrite the letter.",
"greetingMessage": null,
"usageContext": "workflow",
"documents": []
}
A grid template is one file beside the manifest (kind: grid-template):
template/template.json:
{
"name": "Lease facts (example)",
"description": "The facts to pull from every lease in a folder.",
"modelId": "gpt-oss-120b",
"columns": [
{ "label": "Tenant", "prompt": "Who is the tenant?", "shape": "text", "order": 0 },
{ "label": "Break clause", "prompt": "Does the lease contain a break clause?", "shape": "yesNo", "order": 1 },
{ "label": "Term ends", "prompt": "On what date does the term end?", "shape": "date", "order": 2 },
{ "label": "Annual rent", "prompt": "What is the annual rent, as a number?", "shape": "number", "order": 3 }
]
}
Make the zip
Zip manifest.json and the folders beside it, not a folder that holds them: the import looks for manifest.json at the top of the zip.
- Windows: select the manifest and the folders, right-click and choose Compress to ZIP file.
- macOS: select them, Control-click and choose Compress.
- PowerShell 7, in the folder that holds them:
Compress-Archive -Path manifest.json, workflow, agents -DestinationPath letter-review.zip(name the folders the bundle has). - Terminal on macOS or Linux, in the same folder:
zip -r letter-review.zip manifest.json workflow agents.
If the import is refused
| The import says | What to do |
|---|---|
| The file is not a Ferrith bundle, or has no manifest | manifest.json is not at the top of the zip, or is not valid JSON. |
| The bundle was written by a newer version | Its formatVersion is above the workspace's; write the file for format 1. |
| The workspace's agent limit would be exceeded | Fewer agents, or a higher limit from your administrator. |
| The workspace's storage limit would be exceeded | Smaller or fewer knowledge documents. |
| A document was rejected | Its mimeType is not accepted, or the file is over the size or page limit. |
| The workflow is invalid | The YAML failed the editor's checks — the message names the step and the rule. Nothing was created. |
The schema
One schema per file. Each is published at its $id, so a tool can fetch it; a code editor pointed at the right one checks the file as you type.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://docs.ferrith.ai/reference/schemas/bundle-manifest.json",
"title": "Ferrith bundle: manifest.json",
"type": "object",
"required": ["formatVersion", "kind", "name"],
"additionalProperties": false,
"properties": {
"formatVersion": { "const": 1, "description": "The file format version." },
"kind": { "enum": ["workflow", "agent", "grid-template"], "description": "What the bundle holds." },
"name": { "type": "string", "minLength": 1, "description": "The name the import shows." },
"exportedAtUtc": { "type": "string", "format": "date-time", "description": "When the file was written, in UTC." },
"entityNames": { "type": "array", "items": { "type": "string" }, "description": "The names of everything the bundle creates." },
"notices": { "type": "array", "items": { "type": "string" }, "description": "Notes shown to the person importing the file." }
}
}
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://docs.ferrith.ai/reference/schemas/bundle-workflow.json",
"title": "Ferrith bundle: workflow/workflow.json",
"type": "object",
"required": ["name"],
"additionalProperties": false,
"properties": {
"name": { "type": "string", "minLength": 1, "description": "The workflow's name; suffixed on import when the workspace has one already." },
"description": { "type": ["string", "null"], "description": "Shown on the workflow's card." }
}
}
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://docs.ferrith.ai/reference/schemas/bundle-agent.json",
"title": "Ferrith bundle: agents/<key>/agent.json",
"type": "object",
"required": ["key", "name", "usageContext", "documents"],
"additionalProperties": false,
"properties": {
"key": { "type": "string", "pattern": "^agent-[0-9]+$", "description": "The bundle key — the folder's name, and what a workflow step's agentId names." },
"name": { "type": "string", "minLength": 1 },
"description": { "type": ["string", "null"] },
"systemPrompt": { "type": ["string", "null"], "description": "The system prompt as the agent editor shows it." },
"greetingMessage": { "type": ["string", "null"], "description": "The first message a conversation with the agent shows." },
"usageContext": { "enum": ["chat", "workflow"], "description": "An agent people talk to, or one that works inside workflows." },
"documents": {
"type": "array",
"description": "The knowledge documents; each has a folder beside this file holding document.json and binary.",
"items": { "$ref": "https://docs.ferrith.ai/reference/schemas/bundle-document.json" }
}
}
}
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://docs.ferrith.ai/reference/schemas/bundle-document.json",
"title": "Ferrith bundle: agents/<key>/documents/<docKey>/document.json",
"type": "object",
"required": ["key", "filename", "mimeType"],
"additionalProperties": false,
"properties": {
"key": { "type": "string", "pattern": "^doc-[0-9]+$", "description": "The document's key — its folder's name." },
"filename": { "type": "string", "minLength": 1, "description": "The name the document shows in the workspace." },
"mimeType": { "type": "string", "minLength": 1, "description": "The file's type, as an upload declares it: application/pdf, a Word document, Markdown or plain text." }
}
}
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://docs.ferrith.ai/reference/schemas/bundle-grid-template.json",
"title": "Ferrith bundle: template/template.json",
"type": "object",
"required": ["name", "columns"],
"additionalProperties": false,
"properties": {
"name": { "type": "string", "minLength": 1 },
"description": { "type": ["string", "null"] },
"modelId": { "type": ["string", "null"], "description": "The model the questions run on; the workspace's default when it is not served." },
"columns": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["label", "prompt", "shape", "order"],
"additionalProperties": false,
"properties": {
"label": { "type": "string", "minLength": 1, "description": "The column heading." },
"prompt": { "type": "string", "minLength": 1, "description": "The question asked of every document." },
"shape": { "enum": ["text", "yesNo", "date", "number"], "description": "The kind of answer." },
"order": { "type": "integer", "minimum": 0, "description": "The column's position, from 0." }
}
}
}
}
}