AI Agent (AI Pipeline) — Authoring Specification
Purpose of this document. This is a complete, self-contained reference for the JSON structure of an AI Agent (internally called an AI Pipeline). Hand this file to any LLM (Claude, GPT, Gemini, Grok, …) together with a plain-language description of the automation you want, and it should be able to produce a valid AI Agent JSON that can be imported into the platform via AI Agents → Import.
Companion document: JOURNEY_FLOW_SPEC.md (the "Journey" / Flow system). The two systems are siblings with an almost identical JSON shape but different node catalogs. Read the "AI Agent vs Journey" section below to pick the right one.
1. What an AI Agent is
An AI Agent is a directed graph of nodes (a DAG) that runs once per inbound customer message on a channel (WhatsApp, Instagram, or Web Chat). It is designed for AI-first automation: calling LLMs, retrieving knowledge, classifying intent, extracting parameters, branching on logic, and replying.
Execution model (important — it shapes how you must design the graph):
The engine loads the graph, finds the entry node, and traverses node → node by following transitions until it reaches a node with no onward transition (or an
endnode).Traversal is synchronous within a single inbound message. One customer message = one pass through the graph.
The only way the engine "waits for the user" mid-graph is via an interactive node (buttons / list). When such a node is sent, the engine saves a wait state and stops. The next inbound message resumes from that node, routing by which button/row was tapped.
All other nodes execute immediately and continue to the next node in the same pass.
A loop-protection cap of 50 node executions per message aborts runaway graphs.
State is accumulated in a context object (a flat key→value map of "variables") that persists across messages for the life of the chat session. Nodes read variables via {{variable}} interpolation and write variables to named output variables.
AI Agent vs Journey — which to use?
AI Agent (AI Pipeline)Journey (Flow)Primary useAI-driven: LLM replies, RAG, intent routingDeterministic scripted menus & rulesMental model"Reason then respond" DAG"Menu tree / decision tree"Waiting for userInteractive buttons/list onlyButtons, lists, and wait_input (free-text capture)Import formatorquesta-pipeline-v1orquesta-flow-v1DB tablesAiPipeline / AiPipelineNodeFlow / FlowNode
They can call each other: an AI Agent can hand control to a Journey (execute_flow node), and a Journey can hand control to an AI Agent through its own mechanisms.
2. Top-level JSON structure (the import file)
This is exactly what the export endpoint produces and the import endpoint accepts.
{
"_format": "orquesta-pipeline-v1",
"name": "Customer Support Agent",
"description": "Answers FAQs from the knowledge base, escalates on request",
"channel": "WHATSAPP",
"sessionTimeoutMinutes": null,
"entryNodeId": "start_1",
"nodes": [
{ "id": "start_1", "type": "start", "name": "Start", "content": {}, "positionX": 0, "positionY": 0, "transitions": { "default": "llm_1" } },
{ "id": "llm_1", "type": "llm", "name": "Answer", "content": { /* … */ }, "positionX": 300, "positionY": 0, "transitions": { "default": "reply_1" } },
{ "id": "reply_1", "type": "text_response", "name": "Reply", "content": { "message": "{{llm_response}}" }, "positionX": 600, "positionY": 0, "transitions": {} }
],
"exportedAt": "2026-07-07T00:00:00.000Z"
}
Top-level fields
FieldTypeRequiredNotes_formatstringYesMust be exactly "orquesta-pipeline-v1". Import rejects anything else.namestringYesAgent name. On import it is suffixed with (imported).descriptionstring | nullNoFreeform.channelenumNo (default WHATSAPP)One of WHATSAPP, INSTAGRAM, WEBCHAT. Governs which node types are valid (see §7).sessionTimeoutMinutesint | nullNoPer-agent session timeout override in minutes; null = use org default.entryNodeIdstringYes (effectively)The id of the node where traversal begins. Should point at the start node. If missing/unresolved, the agent does nothing.nodesarrayYesArray of node objects (see §3). Must be a real array.exportedAtstring (ISO)NoIgnored on import; include it for round-tripping.
On import, every node
idis regenerated and allentryNodeId+transitionsreferences are remapped automatically. Therefore, when authoring by hand, you may use any unique string ids you like (e.g.start_1,llm_answer,n_ask_name). They only need to be unique within the file and referenced consistently byentryNodeIdand bytransitions. Readable slug ids are recommended for hand-authoring.
3. The node object
Every element of nodes[] has this shape (matches AiPipelineNode):
{
"id": "unique_node_id",
"type": "llm",
"name": "Optional human label",
"content": { },
"positionX": 300,
"positionY": 120,
"transitions": { "default": "next_node_id" }
}
FieldTypeRequiredNotesidstringYesUnique within the file.typestringYesOne of the node types in §6. Must match the channel (§7).namestring | nullNoDisplay label only.contentobjectYesType-specific configuration. Shape defined per node in §6. May be {} for start/end.positionX / positionYnumberNo (default 0)Canvas coordinates for the visual editor. Use a left→right layout (e.g. increment X by ~300 per step) so the imported graph is readable. Purely cosmetic.transitionsobject | nullNokey → target-node-id map. See §4.
4. Transitions — how nodes connect (READ THIS CAREFULLY)
transitions is a plain object mapping a branch key to a target node id. The branch key depends on the node type. Getting these keys right is the single most important part of authoring.
4.1 Linear nodes — default
Most nodes (start, llm, text_response, set_variable, http_request, media nodes, etc.) have exactly one outgoing path. They follow transitions.default:
"transitions": { "default": "next_node_id" }
If default is missing/empty, traversal stops after that node (a valid way to end).
4.2 condition node — one key per condition id, plus default
Each entry in content.conditions[] has an id. The transition key is that id. Add a default for the else-branch:
"content": {
"conditions": [
{ "id": "is_vip", "name": "VIP", "expression": "{{tier}} == \"vip\"" },
{ "id": "is_angry", "name": "Angry", "expression": "{{sentiment}} == \"negative\"" }
]
},
"transitions": {
"is_vip": "vip_handler",
"is_angry": "escalate",
"default": "normal_handler"
}
Conditions are evaluated top-to-bottom; first match wins. If none match, default is used.
4.3 question_classifier node — one key per class id, plus default
The LLM classifies the input into one of content.classes[] (by id). Route each class id, plus a default fallback:
"content": {
"modelId": "<model-id>",
"inputVariable": "query",
"classes": [
{ "id": "billing", "name": "Billing", "description": "Payments, invoices, refunds" },
{ "id": "support", "name": "Support", "description": "Technical problems" }
]
},
"transitions": { "billing": "billing_node", "support": "support_node", "default": "fallback_node" }
Class keys are matched case-insensitively.
4.4 Interactive nodes — one key per button/row id, plus default (these WAIT)
interactive_response, multi_interactive_response, interactive_list_response, web_interactive_response, web_list_response: after sending, the engine pauses and waits for the next inbound message. When the user taps a button (or picks a list row), it resumes and routes via transitions[<buttonId or rowId>], falling back to transitions.default.
"content": {
"body": "How can we help?",
"buttons": [
{ "id": "sales", "title": "Talk to Sales" },
{ "id": "support", "title": "Get Support" }
]
},
"transitions": { "sales": "sales_node", "support": "support_node", "default": "reprompt_node" }
If a button/row has no matching transition and no
default, the reply is ignored and the agent keeps waiting on the same node.
4.5 iteration node — body and done
"content": { "arrayVariable": "items", "itemVariable": "item", "indexVariable": "i", "maxIterations": 100 },
"transitions": { "body": "process_item_node", "done": "after_loop_node" }
body→ the first node of the loop body. The last node of the loop body must transition back to theiterationnode ("default": "<iteration node id>") so it can advance to the next item.done(ordefault) → node to run after the loop finishes. Cap: 100 iterations.
4.6 Terminal nodes (no transitions needed)
end— stops the pipeline.handoff— hands the chat to a human agent.execute_flow— hands control to a Journey (Flow); the Flow now owns the chat.
These don't need outgoing transitions (any are ignored).
5. Variables, interpolation & expressions
5.1 Interpolation syntax — {{ … }}
Anywhere a content string is sent or evaluated, {{variableName}} is replaced with the value from context. Supported accessors:
Simple:
{{query}},{{contact_name}}Dot notation into JSON:
{{order.total}},{{profile.address.city}}Bracket notation:
{{response["data"][0]["name"]}}Nested (resolved innermost-first):
{{pricing["{{kecamatan}}"]}}
If a variable is a JSON string, it is auto-parsed before accessor traversal. Missing variables resolve to an empty string. Values that are objects/arrays are stringified as JSON.
5.2 Built-in variables (available at the start of every run)
VariableTypeMeaningquerystringThe inbound message text (or media caption).filestring | nullMedia attachment URL, if the inbound message had media.file_typestring | nullimage | video | audio | document.contact_namestringContact display name.contact_phonestringWhatsApp ID (WA) or Instagram ID (IG).contact_ig_usernamestringInstagram username (IG only).message_typestringtext, image, button_reply, list_reply, quick_reply, postback, …button_payloadstring | nullThe id/payload of a tapped button, if any.channelstringWHATSAPP | INSTAGRAM | WEBCHAT.
Any variable your nodes write (LLM outputVariable, set_variable, http_requestoutputVariable, etc.) is added to context and reusable downstream in the same run and in later messages of the same session.
5.3 Condition expressions
Used by condition node expressions and the set_variable expression value type. A condition expression is a single comparison:
<left> <operator> <right>
Operators: ==, !=, ===, !==, >, <, >=, <=. Operands may be {{variables}}, quoted strings ("vip"), numbers, or booleans (true/false). Examples:
{{tier}} == "vip"
{{age}} >= 18
{{count}} != 0
{{opted_in}} == true
5.4 set_variable / code value expressions (safe evaluator)
The set_variable node with valueType: "expression" and the code node use a restricted evaluator (not full JS). Supported:
Number literals:
42,3.14JSON literals:
{"a":1},[1,2,3](great for seeding lookup tables)Variable references:
myVarProperty access:
obj.prop,obj["prop"]A few string methods:
.toUpperCase(),.toLowerCase(),.trim(),.length,.toString()Array
.lengthJSON.stringify(x)/JSON.parse(x)Math.abs/round/floor/ceil/min/max/pow/sqrt(...)Single binary arithmetic:
a + b,a - b,a * b,a / b
The code node also supports language: "jsonata" for richer expressions (array map/reduce, $sum, $join, multi-term arithmetic, etc.).
6. Node type catalog
For each node: purpose, content schema (required fields bold), allowed values, transition keys, and an example. ? marks optional fields. Unless noted, string fields support {{variable}} interpolation.
6.1 Control & logic
start
Entry point. Pass-through.
content:{}(empty).Transitions:
{ "default": "<first node>" }.entryNodeIdmust equal this node's id.
end
Stops the pipeline.
content:{}.Transitions: none.
condition
Branch on expressions. See §4.2.
"content": {
"conditions": [ { "id": "c1", "name": "label", "expression": "{{x}} == \"y\"" } ],
"defaultTargetNodeId": ""
}
conditions[]: each{ id, name, expression }.idrequired (used as transition key).Transitions: one per condition
id+default.
set_variable
Assign one or more context variables.
"content": {
"assignments": [
{ "variableName": "greeting", "valueType": "static", "value": "Hi {{contact_name}}" },
{ "variableName": "total", "valueType": "expression", "value": "{{price}} * {{qty}}" }
]
}
assignments[]: each{ variableName, valueType, value }.valueType:"static"(interpolate{{}}then store as string) or"expression"(safe evaluator, §5.4).
Transitions:
default.
variable_aggregator
Merge several source variables into one (single source → value; multiple → array).
"content": { "variableName": "all_answers", "sources": [ { "variableName": "a1" }, { "variableName": "a2" } ] }
Transitions:
default.
iteration
Loop over an array. See §4.5.
"content": { "arrayVariable": "items", "itemVariable": "item", "indexVariable": "index", "maxIterations": 100 }
arrayVariable: name of a context variable holding an array (or JSON-array string).itemVariable/indexVariable: where the current item/index are written each pass.maxIterations: default & hard cap 100.Transitions:
body(loop body start) +done(ordefault). Loop body's last node must transition back to this node.
delay
Blocking wait, then continue.
"content": { "duration": 5, "unit": "seconds", "maxDelaySeconds": 300 }
duration+unit("seconds"|"minutes"). Capped atmaxDelaySeconds(default 300s).Skipped entirely in test/dry-run mode.
Transitions:
default.
6.2 AI / LLM
llm
Call an LLM. The core node of most agents.
"content": {
"modelId": "<AiModel id from your org's model catalog>",
"systemPrompt": "You are a helpful support agent for Acme.",
"userPrompt": "{{query}}",
"temperature": 0.7,
"maxTokens": 1024,
"memory": { "enabled": true, "maxMessages": 20, "windowType": "message_count" },
"contextVariables": ["kb_results"],
"vision": { "enabled": false, "detail": "low" },
"structuredOutput": { "enabled": false, "schema": {}, "outputVariable": "llm_structured" },
"thinking": { "enabled": false, "outputVariable": "llm_thinking" },
"outputVariable": "llm_response"
}
modelId— the model identifier from the org's AI model catalog (AiModel.id). This is environment-specific; leave the picker to the UI or obtain a valid id from the target org. When authoring blind, use a placeholder like"<MODEL_ID>"and instruct the user to select the model in the editor.userPrompt— required.systemPromptoptional. Both interpolate{{}}.temperature— 0–2 (clamped), default 0.7.maxTokens— optional; defaults to the model's configured max.memory—{ enabled, maxMessages, windowType }.windowType:"message_count"(take lastmaxMessages) or"session_lifetime"(all messages this session, hard-capped at 50). History is injected as a framed block in the system prompt.contextVariables[]— names of variables (typically knowledge-retrieval output) whose contents are injected as a<context>block into the user prompt for grounding.vision—{ enabled, detail }(detail:"low"|"high"). Only used if the inboundfileis an image URL and the model supports vision.structuredOutput—{ enabled, schema, outputVariable? }.schemais a JSON Schema object. When enabled (and supported),outputVariablegets the full JSON string; the plainoutputVariablegets thereply_textfield. Ignored if the model doesn't support it or if thinking is on.thinking—{ enabled, outputVariable? }. Captures model reasoning. Mutually exclusive with structured output.outputVariable— where the reply text is stored (e.g.llm_response).Transitions:
default.Failure behavior: if out of AI credits or the call errors, the node halts the pipeline (does not advance, does not echo an error to the customer).
knowledge_retrieval
RAG search against knowledge datasets; store results for an llm node to ground on.
"content": {
"datasetIds": ["<KnowledgeDataset id>", "..."],
"queryVariable": "query",
"topK": 5,
"scoreThreshold": 0.5,
"outputVariable": "kb_results"
}
datasetIds[]— knowledge dataset ids (environment-specific).queryVariable— context var holding the search query (defaultquery).topK— results per dataset (default 5).scoreThreshold— min relevance 0–1 (default 0.5;0disables the floor).outputVariable— stores a JSON array of{ text, documentName, score }. Feed this variable name into a downstreamllmnode'scontextVariables.Transitions:
default. (Typical pattern:knowledge_retrieval→llm→text_response.)
question_classifier
LLM routes input into one of several classes. See §4.3.
"content": { "modelId": "<MODEL_ID>", "inputVariable": "query",
"classes": [ { "id": "billing", "name": "Billing", "description": "…" } ] }
Transitions: one per class
id+default.
parameter_extractor
LLM extracts structured parameters from input into a JSON object.
"content": {
"modelId": "<MODEL_ID>",
"inputVariable": "query",
"parameters": [
{ "name": "name", "type": "string", "description": "Customer full name", "required": true },
{ "name": "email", "type": "string", "description": "Email address", "required": false }
],
"outputVariable": "extracted_params"
}
parameters[].type:"string"|"number"|"boolean".outputVariablestores a JSON string; access fields via{{extracted_params.name}}.Transitions:
default.
6.3 Integration
http_request
Call an external API.
"content": {
"method": "POST",
"url": "https://api.example.com/lookup?id={{customer_id}}",
"headers": [ { "key": "Authorization", "value": "Bearer {{token}}" } ],
"params": [ { "key": "q", "value": "{{query}}" } ],
"bodyType": "json",
"body": "{ \"name\": \"{{contact_name}}\" }",
"timeout": 10000,
"outputVariable": "http_response"
}
method:GET|POST|PUT|DELETE|PATCH.urlrequired.bodyType:none|json|form-data|raw.bodyinterpolates{{}}(JSON-safe escaping forjson).timeoutms.outputVariablestores the parsed response — access via{{http_response["field"]}}.Transitions:
default.
code
Evaluate a restricted expression (§5.4) and store the result.
"content": { "expression": "{{price}} * {{qty}}", "inputVariables": ["price","qty"], "outputVariable": "total", "language": "legacy" }
language:"legacy"(default, restricted evaluator) or"jsonata"(richer).Transitions:
default.
6.4 Response / messaging (WhatsApp & Instagram)
On Instagram,
interactive_responseis delivered as quick replies;multi_interactive_responseandinteractive_list_responseare WhatsApp-only and are skipped on IG.
text_response
Send a text message.
"content": { "message": "{{llm_response}}" }
messagerequired, interpolates{{}}. Transitions:default.
interactive_response
Send up to 3 reply buttons, then wait for a tap. See §4.4.
"content": { "header": "", "body": "Choose:", "footer": "",
"buttons": [ { "id": "opt1", "title": "Option 1" }, { "id": "opt2", "title": "Option 2" } ] }
bodyrequired.buttons[]: each{ id, title }(title ≤ 20 chars; max 3 buttons).Transitions: one per button
id+default.
multi_interactive_response (WhatsApp only)
Send a sequence of button messages (each ≤ 3 buttons), then wait for any tap.
"content": { "messages": [
{ "body": "Page 1", "buttons": [ {"id":"a","title":"A"}, {"id":"b","title":"B"}, {"id":"c","title":"C"} ] },
{ "body": "Page 2", "buttons": [ {"id":"d","title":"D"}, {"id":"e","title":"E"} ] }
] }
Transitions: one key per button
idacross all messages +default.
interactive_list_response (WhatsApp only)
Send a list menu, then wait for a row selection.
"content": { "header": "", "body": "Pick one:", "footer": "", "buttonText": "Menu",
"sections": [ { "title": "Options", "rows": [ { "id": "r1", "title": "Row 1", "description": "" } ] } ] }
body,buttonText,sections[]required. Each row{ id, title, description? }.Transitions: one per row
id+default.
Media nodes: send_image, send_video, send_audio, send_document
"content": { "sourceType": "url", "url": "https://…/file.jpg", "caption": "Optional" }
sourceType:"url"or"upload". Forurl, seturl. Forupload, the editor setsmediaId/objectPath(not hand-authorable).send_documentalso hasfileName.send_audiohas no caption.Transitions:
default.
send_location
"content": { "latitude": "-6.20", "longitude": "106.81", "name": "Office", "address": "Jakarta" }
latitude,longituderequired (strings, interpolatable). Transitions:default.
send_contacts
Send one or more contact cards.
"content": { "contacts": [ { "name": { "formatted_name": "John Doe", "first_name": "John" },
"phones": [ { "phone": "+62812…", "type": "CELL" } ] } ] }
Each contact requires
name.formatted_name. Transitions:default.
send_cta_url
Interactive message with a URL button.
"content": { "headerType": "none", "body": "Learn more:", "footer": "", "buttonText": "Open", "buttonUrl": "https://example.com" }
body,buttonText(≤20 chars),buttonUrlrequired.headerType:none|text|image|document(+ matchingheaderText/headerImageUrl/headerDocumentUrl).Transitions:
default.
send_whatsapp_native_flow (WhatsApp only)
Send a WhatsApp Flow (native form).
"content": { "nativeFlowId": "<flow id>", "bodyText": "Complete this form", "ctaText": "Open",
"headerText": "", "footerText": "", "startScreenId": "SCREEN_ONE", "flowAction": "navigate",
"prefill": [ { "key": "name", "value": "{{contact_name}}" } ] }
nativeFlowIdrequired (environment-specific).flowAction:navigate|data_exchange.prefillvalues interpolate. Transitions:default.
6.5 Web Chat response nodes (WEBCHAT channel only)
Use these instead of the WhatsApp/IG response nodes when channel is WEBCHAT:
web_text_response—{ "message": "…" }web_interactive_response— same shape asinteractive_response(buttons; waits).web_list_response— same shape asinteractive_list_response(list; waits).
Transition rules are identical to their WhatsApp equivalents (§4.4).
6.6 Catalog nodes (WhatsApp only)
All take a local catalogId (ProductCatalog.id, environment-specific).
TypePurposeKey content fieldsTransitionssend_single_productSend one productcatalogId, productRetailerId, bodyText?, footerText?defaultsend_multi_productProduct list messagecatalogId, headerText, bodyText, sections[] ({title, productRetailerIds[]})defaultsend_catalogFull catalog linkcatalogId, bodyText, thumbnailProductRetailerIddefaultsearch_catalog_productSearch → store matchescatalogId, searchQuery, searchField (name|retailer_id|category|all), maxResults, outputVariabledefaultget_product_listFetch list → storecatalogId, maxResults, outputVariabledefaultget_single_productFetch one → storecatalogId, productRetailerId, outputVariabledefault
productRetailerId / searchQuery interpolate {{}} (e.g. "{{query}}").
6.7 Actions & handoff
perform_action
Run one or more side-effecting actions, then branch success/failure.
"content": {
"actions": [
{ "action": "contact_field_set", "fieldLabel": "email", "fieldValue": "{{extracted_params.email}}" },
{ "action": "ticket_create", "ticketTitle": "Support: {{query}}", "ticketPriority": "MEDIUM", "ticketNumberVariable": "ticket_no" }
],
"errorMode": "fail_fast"
}
actions[]runs steps sequentially. (Legacy: a single action's fields may live directly oncontentwith noactions[].)actionvalues:contact_field_set,contact_field_delete,contact_field_read,segment_add,end_session,ticket_create,ticket_modify,ticket_read.Per-action fields (only set the ones relevant to the chosen action):
Contact fields:
fieldLabel,fieldValue,fieldOutputVariable(read → stores value).segment_add:segmentId.end_session:endMessage.ticket_create:ticketTitle,ticketPriority(LOW|MEDIUM|HIGH|URGENT),ticketNumberVariable.ticket_modify/ticket_read:ticketTargetMode(variable|contact_latest),ticketNumberSource,ticketStatus,ticketSetPriority,ticketAssigneeId,ticketNote,ticketOutputPrefix.
errorMode:fail_fast(default) |continue.Transitions:
successandfailure(fall back todefaultif you only have one path).
handoff
Escalate the chat to a human. Terminal.
"content": { "message": "Connecting you with an agent…", "assignmentMode": "none", "assignToAgentId": "" }
assignmentMode:none(escalate only) |specific(assign toassignToAgentId) |auto(run org assignment rules). Transitions: none needed.
execute_flow (WhatsApp only)
Hand control to a Journey (Flow). Terminal for the agent.
"content": { "selectionMode": "static", "targetFlowId": "<Flow id>", "targetFlowVariable": "", "passContext": true }
selectionMode:static(usetargetFlowId) |dynamic(use variable named intargetFlowVariable).passContext: carry current variables into the Flow's initial context.
6.8 Email
send_email
"content": { "to": "ops@acme.com,{{contact_email}}", "subject": "New lead: {{contact_name}}", "body": "…\n…", "replyTo": "" }
to(comma-separated),subject,bodyrequired; all interpolate. Transitions:default.
7. Channel rules (which node types are valid)
The valid node palette depends on the agent's channel:
WHATSAPP / INSTAGRAM: all logic/AI/integration nodes +
text_response,interactive_response,multi_interactive_response,interactive_list_response, media nodes,send_cta_url,send_whatsapp_native_flow, catalog nodes,perform_action,handoff,execute_flow*,send_email. Do not use theweb_*nodes. (*= WhatsApp-only; skipped on Instagram.)WEBCHAT: logic/AI/integration nodes +
web_text_response,web_interactive_response,web_list_response,perform_action,handoff,execute_flow,send_email. Do not use the WhatsApp/IG message or catalog nodes.
8. Complete worked example
A WhatsApp support agent that: retrieves knowledge → answers with an LLM → offers a "talk to human" button → escalates on request.
{
"_format": "orquesta-pipeline-v1",
"name": "Support Agent with KB + Handoff",
"description": "RAG answer, then offer human handoff",
"channel": "WHATSAPP",
"sessionTimeoutMinutes": null,
"entryNodeId": "start",
"nodes": [
{ "id": "start", "type": "start", "name": "Start", "content": {},
"positionX": 0, "positionY": 0, "transitions": { "default": "kb" } },
{ "id": "kb", "type": "knowledge_retrieval", "name": "Search KB",
"content": { "datasetIds": ["<DATASET_ID>"], "queryVariable": "query", "topK": 5, "scoreThreshold": 0.5, "outputVariable": "kb_results" },
"positionX": 300, "positionY": 0, "transitions": { "default": "answer" } },
{ "id": "answer", "type": "llm", "name": "Answer",
"content": {
"modelId": "<MODEL_ID>",
"systemPrompt": "You are Acme's support assistant. Answer only from the provided context. If unsure, say you'll connect a human.",
"userPrompt": "{{query}}",
"temperature": 0.3,
"memory": { "enabled": true, "maxMessages": 10, "windowType": "message_count" },
"contextVariables": ["kb_results"],
"vision": { "enabled": false, "detail": "low" },
"outputVariable": "llm_response"
},
"positionX": 600, "positionY": 0, "transitions": { "default": "reply" } },
{ "id": "reply", "type": "text_response", "name": "Send answer",
"content": { "message": "{{llm_response}}" },
"positionX": 900, "positionY": 0, "transitions": { "default": "offer" } },
{ "id": "offer", "type": "interactive_response", "name": "Offer handoff",
"content": { "body": "Did that help, or would you like to talk to a person?",
"buttons": [ { "id": "human", "title": "Talk to a human" }, { "id": "done", "title": "All good" } ] },
"positionX": 1200, "positionY": 0,
"transitions": { "human": "escalate", "done": "bye", "default": "bye" } },
{ "id": "escalate", "type": "handoff", "name": "Escalate",
"content": { "message": "Connecting you with an agent…", "assignmentMode": "auto" },
"positionX": 1500, "positionY": -100, "transitions": {} },
{ "id": "bye", "type": "text_response", "name": "Goodbye",
"content": { "message": "Thanks for reaching out! 👋" },
"positionX": 1500, "positionY": 100, "transitions": {} }
],
"exportedAt": "2026-07-07T00:00:00.000Z"
}
9. Authoring checklist (validate before importing)
_formatis exactly"orquesta-pipeline-v1";nameandnodes[]are present.There is exactly one
startnode andentryNodeIdequals itsid.Every node
idis unique.Every id referenced in
entryNodeIdand in anytransitionsvalue exists innodes[].Transition keys match the node type's contract (§4):
defaultfor linear; condition ids; class ids; button/row ids;body/done;success/failure.Interactive/list nodes that expect a reply have a transition for each button/row id, plus a
default.Loop bodies (
iteration) end by transitioning back to the iteration node.Node types are valid for the declared
channel(§7).Required
contentfields are present for each node type (bold fields in §6).Environment-specific ids (
modelId,datasetIds,catalogId,nativeFlowId,assignToAgentId,targetFlowId,segmentId) are either real values for the target org or clearly-marked placeholders for the user to fill in the editor.Keep the graph small enough to stay well under the 50-node-execution cap per message.