Documentation Index
Fetch the complete documentation index at: https://docs.goyappr.com/llms.txt
Use this file to discover all available pages before exploring further.
Flow agents (type: "flow") carry a flow_config JSONB on the agent row. This page is the canonical reference for that shape. The same schema is validated server-side by the public API, by the dashboard’s React Flow builder, and by the bot pipeline at call-start time.
Top-level shape
{
"flow_config_version": "1",
"nodes": [ /* see node types below */ ],
"metadata": { /* optional, surfaced to the eval LLM */ }
}
Validation gates (enforced before save)
- Exactly one node with
type: "start".
- Every
next_step_id (transitions, custom branches, start) resolves to an existing node id.
- Node ids are unique within the graph.
- Every node passes its own per-type schema (see below).
A failed gate returns 400 with a human-readable error message pointing at the offending field.
Node types
Every node has:
{
"id": "stable-id",
"type": "<one of: start | conversation | tool_call | integration_call | transfer | end>",
"name": "Display name (canvas label)",
"position": { "x": 0, "y": 0 } // optional, UI-only
}
start
Entry point. Owns the “who speaks first” decision for flow agents — these settings override the agent-level agent.agent_speaks_first and agent.greeting_message fields when the agent is a flow agent.
{
"id": "start-1",
"type": "start",
"agent_speaks_first": true,
"greeting": "שלום, חברת ירוק מדבר",
"is_literal": false,
"next_step_id": "ask-name-1",
"auto_advance": true
}
| Field | Description |
|---|
agent_speaks_first | Default true. When false, the bot waits silently for the caller to speak first (silence_timeout_secs on the agent still applies). |
greeting | Spoken greeting — only used when agent_speaks_first: true. |
is_literal | When true, greeting is spoken verbatim. When false, it’s treated as an LLM instruction. |
next_step_id | The conversation node the bot enters after greeting (or after the user’s first turn, if agent_speaks_first: false). |
auto_advance | Default true. Whether to enter the first conversation node immediately on session start. When false, the bot’s greeting is delivered in start-node context only; the first conversation node is entered only after the user’s first reply (via internal advancement). |
Use auto_advance: false when the greeting should feel neutral and the bot
shouldn’t enter its first scripted step until the user has spoken. Useful for
agents that listen for an open-ended intent (caller’s reason for calling)
before routing — the greeting stays generic, then the first conversation
node’s instructions take effect once the caller actually replies.
conversation
Talks to the user, then picks a transition.
{
"id": "ask-name-1",
"type": "conversation",
"name": "Ask name",
"instructions": "Ask the caller for their full name. Confirm the spelling if Hebrew sounds ambiguous.",
"transitions": [
{ "id": "tx-got-name", "label": "Caller gave their name", "next_step_id": "ask-date-1" },
{ "id": "tx-refused-name", "label": "Caller refused", "next_step_id": "polite-end-1" }
]
}
The instructions text is layered on top of the agent’s global system_prompt as a system message at step entry. transitions[].label (and optional description) are fed to the eval LLM as the choice menu.
Deterministic tool execution. References a tool by tool_id (must already exist in the company’s tools table). Tool args are owned by the tool itself via payload_config.static_parameters (literals) and payload_config.extraction_parameters (filled by the runtime from the conversation). A tool_call node carries no args_template field — the same tool used by N flow nodes always sends the same shape; if you need a different shape per step, create a separate tool. Stale args_template payloads on a tool_call node are silently dropped at parse time.
{
"id": "check-avail-1",
"type": "tool_call",
"name": "Check Availability",
"tool_id": "11111111-1111-1111-1111-111111111111",
"config_override": null,
"transitions": {
"success_next_step_id": "confirm-1",
"error_next_step_id": "apologize-1",
"custom": [
{
"id": "tx-no-slots",
"label": "No availability",
"jsonpath": "$.status",
"equals": "no_availability",
"next_step_id": "offer-alt-1"
}
]
},
"pre_fire_announcement": true,
"timeout_secs": 30
}
config_override shallow-merges over the referenced tool’s config (array replacement, not deep merge). Open-shape: each tool type defines its own valid keys.
pre_fire_announcement plays a short platform-controlled hold tone while the webhook is in flight. Recommended for tools that may take more than ~500 ms.
timeout_secs (1–300) caps how long the dispatcher waits before cancelling the call and routing to error_next_step_id with tool_timeout_after_<N>s. Null/omitted = 30s default. Bump up for slow webhooks.
Args come from the linked tool, not the node. Tool-call nodes do not carry an args_template. Each extraction_parameters entry on the linked tool is AI-extracted from the conversation at fire time; each static_parameters entry is included verbatim. If a required extraction_parameters value can’t be surfaced after 3 attempts, the node routes to error_next_step_id.
Tool-call nodes can be token sources for downstream nodes: any extraction_parameters[].name becomes addressable as {{<node_id>.<name>}} in later integration_call.args_template literals or ai_extract.description strings.
How tool-call routing works at runtime — entirely deterministic, no LLM involved:
error_next_step_id fires only on hard failures: network timeout, exception, HTTP 4xx/5xx, integration disconnected, tool deleted/inactive, missing required config. The dispatcher’s error field is set; nothing else is checked.
- Otherwise the dispatcher walks
custom[] top-to-bottom. First branch whose path extracts a value == its equals wins. Evaluation stops there — success is not also taken.
- If no custom matched,
success_next_step_id fires.
Exactly one out-edge is traversed per tool fire. The canvas drawing all three lines is just the diagram of possible routes — at runtime only one is taken.
The full result dict is injected into the next node’s LLM context as a <tool_result> block, so a single success → conversation node usually handles soft-fail bodies ({"available": false}) gracefully via prompt instructions. Use custom[] only when the next node should be structurally different for that shape (different instructions, different downstream tools, different transitions).
JSONPath subset for custom[].jsonpath
The runtime supports a deliberately tiny subset of JSONPath. Root ($) is the tool’s parsed response body:
- Webhook tools —
JSON.parse(body) of the HTTP response.
- Integration tools — the typed dict returned by the provider client.
| Path | Resolves to |
|---|
$.status | result["status"] |
$.data.appointment.id | nested key drill-down |
$.slots[0].time | array index |
$.appointments[2] | bare array index |
Not supported: recursive descent ($..foo), wildcards ($.*), filter expressions ($[?(@.x>1)]). If your webhook nests the field, point at the exact path. A missing key, wrong type at any step, or out-of-bounds index → the branch silently doesn’t match (falls through to the next custom, then to success).
Stringification rules for equals
The extracted value is stringified JSON-style before string-comparison to equals. Match the table:
| Result value | Write equals: |
|---|
true (boolean) | "true" (lowercase) |
false (boolean) | "false" |
null | "null" |
42 (number) | "42" |
"booked" (string) | "booked" |
Branches with the wrong stringification (e.g. equals: "True" against a boolean true) silently never match.
integration_call
Calls an OAuth-backed third-party integration (Google Calendar, Gmail) directly from the flow — no tools table row required. The integration config (provider, credential, action) lives on the node itself and the args go in args_template. Routing is identical to tool_call (success / error / custom JSONPath, mutually exclusive, exactly one out-edge per fire).
For the full per-node schema, action catalog, arg modes (literal / ai_extract with {{node.arg}} and {{metadata.key}} token interpolation), and the Calendar response post-processing rules, see the dedicated reference: Integration call node.
transfer
Hands the call off via SIP transfer. Terminal — no transitions out.
{
"id": "transfer-1",
"type": "transfer",
"transfer_to": "+972501234567",
"transfer_message": "Connecting you now"
}
end
Speaks the farewell and hangs up. Terminal.
{
"id": "end-1",
"type": "end",
"farewell": "Thanks for calling, goodbye",
"is_literal": false
}
For per-call extraction or webhook delivery, configure extraction_parameters and webhook_url / webhook_events at the agent level. Those mechanisms apply uniformly to both prompt and flow agents — the flow does not have a separate post-end pipeline.
Global nodes
Conversation, transfer, and end nodes can be marked is_global: true to make them reachable from any conversation node without an explicit edge. The eval LLM gets every global node as an extra candidate transition on every turn, with a strong “prefer labeled transitions over global jumps when both could plausibly apply” bias.
Use cases:
- Misclassification recovery — “wait, you’re an owner, not a tenant” routing to the owner-branch entry, declared once instead of wired into every node.
- Universal escape hatches —
transfer to human, or end on do-not-call.
- Session-level concerns — “callback request from anywhere”.
{
"id": "owner_intake",
"type": "conversation",
"name": "Owner intake",
"instructions": "...",
"transitions": [...],
"is_global": true,
"global_jump_description": "User reveals they're actually an owner/landlord, not a tenant"
}
Validation rules (enforced by the API on POST/PATCH):
is_global: true is only valid on conversation, transfer, and end nodes. Setting it on a start or tool_call node returns 400.
- When
is_global: true, global_jump_description must be a non-empty string.
- The current node never jumps to itself even if it’s global.
Practical guidance:
- Recommended max ≤3 globals per flow. Each additional global widens the eval LLM’s candidate space and increases false-positive jump risk.
- Write
global_jump_description as a user-side signal, not an agent intent. Good: “User says they want to speak to a human”; bad: “Transfer the user”.
- Globals don’t replace explicit transitions — they’re a fallback. The eval LLM is instructed to prefer labeled transitions when ambiguous.
When a global jump fires, the runtime publishes flow_node_entered with reason: "global jump: <node name>" (visible in flow_trace.steps[].reason on GET /calls/:id).
What’s NOT a node type
By design, the flow data model omits a few patterns you might expect:
- No “webhook” node — for outbound webhooks, either (a) configure
agent.webhook_url + agent.webhook_events for unconditional per-call delivery (recommended), or (b) call a webhook-type tool from a tool_call node mid-flow if you need per-path delivery.
- No “structured_output” node — for transcript extraction, configure
agent.extraction_parameters. (Per-end-path schemas were considered for v1 but cut to avoid duplicating an agent-level mechanism.)
- No separate “branch” / “ifelse” node — branching is built into every conversation node via
transitions[].
Versioning
Every successful save (POST or PATCH that includes flow_config) writes a flow_versions row. Identical re-saves are deduplicated by SHA-256 hash of the canonical JSON. Use GET /agents/:id/flow/versions to list history.
Immutability
agents.type is immutable post-create. To convert a prompt agent into a flow agent (or vice-versa), create a new agent of the desired type. PATCHing type returns 400.
Type discriminator and flow_config requirements
type | flow_config required? | flow_config allowed? |
|---|
prompt | no | no (must be omitted/null) |
flow | yes | yes |
Mismatches return 400 with a clear correction message.