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 end node).

  • 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 id is regenerated and all entryNodeId + transitions references 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 by entryNodeId and by transitions. 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 the iteration node ("default": "<iteration node id>") so it can advance to the next item.

  • done (or default) → 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.14

  • JSON literals: {"a":1}, [1,2,3] (great for seeding lookup tables)

  • Variable references: myVar

  • Property access: obj.prop, obj["prop"]

  • A few string methods: .toUpperCase(), .toLowerCase(), .trim(), .length, .toString()

  • Array .length

  • JSON.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>" }. entryNodeId must 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 }. id required (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 (or default). 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 at maxDelaySeconds (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. systemPrompt optional. 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 last maxMessages) 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 inbound file is an image URL and the model supports vision.

  • structuredOutput{ enabled, schema, outputVariable? }. schema is a JSON Schema object. When enabled (and supported), outputVariable gets the full JSON string; the plain outputVariable gets the reply_text field. 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 (default query).

  • topK — results per dataset (default 5). scoreThreshold — min relevance 0–1 (default 0.5; 0 disables the floor).

  • outputVariable — stores a JSON array of { text, documentName, score }. Feed this variable name into a downstream llm node's contextVariables.

  • Transitions: default. (Typical pattern: knowledge_retrievalllmtext_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".

  • outputVariable stores 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. url required.

  • bodyType: none|json|form-data|raw. body interpolates {{}} (JSON-safe escaping for json).

  • timeout ms. outputVariable stores 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_response is delivered as quick replies; multi_interactive_response and interactive_list_response are WhatsApp-only and are skipped on IG.

text_response

Send a text message.

"content": { "message": "{{llm_response}}" }
  • message required, 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" } ] }
  • body required. 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 id across 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". For url, set url. For upload, the editor sets mediaId/objectPath (not hand-authorable). send_document also has fileName. send_audio has no caption.

  • Transitions: default.

send_location

"content": { "latitude": "-6.20", "longitude": "106.81", "name": "Office", "address": "Jakarta" }
  • latitude, longitude required (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), buttonUrl required. headerType: none|text|image|document (+ matching headerText/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}}" } ] }
  • nativeFlowId required (environment-specific). flowAction: navigate|data_exchange. prefill values 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 as interactive_response (buttons; waits).

  • web_list_response — same shape as interactive_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 on content with no actions[].)

  • action values: 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: success and failure (fall back to default if 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 to assignToAgentId) | 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 (use targetFlowId) | dynamic (use variable named in targetFlowVariable). 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, body required; 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 the web_* 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)

  1. _format is exactly "orquesta-pipeline-v1"; name and nodes[] are present.

  2. There is exactly one start node and entryNodeId equals its id.

  3. Every node id is unique.

  4. Every id referenced in entryNodeId and in any transitions value exists in nodes[].

  5. Transition keys match the node type's contract (§4): default for linear; condition ids; class ids; button/row ids; body/done; success/failure.

  6. Interactive/list nodes that expect a reply have a transition for each button/row id, plus a default.

  7. Loop bodies (iteration) end by transitioning back to the iteration node.

  8. Node types are valid for the declared channel (§7).

  9. Required content fields are present for each node type (bold fields in §6).

  10. 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.

  11. Keep the graph small enough to stay well under the 50-node-execution cap per message.