Skip to main content

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
}
FieldDescription
agent_speaks_firstDefault true. When false, the bot waits silently for the caller to speak first (silence_timeout_secs on the agent still applies).
greetingSpoken greeting — only used when agent_speaks_first: true.
is_literalWhen true, greeting is spoken verbatim. When false, it’s treated as an LLM instruction.
next_step_idThe conversation node the bot enters after greeting (or after the user’s first turn, if agent_speaks_first: false).
auto_advanceDefault 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.

tool_call

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:
  1. 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.
  2. 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.
  3. 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.
PathResolves to
$.statusresult["status"]
$.data.appointment.idnested key drill-down
$.slots[0].timearray 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 valueWrite 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 hatchestransfer 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

typeflow_config required?flow_config allowed?
promptnono (must be omitted/null)
flowyesyes
Mismatches return 400 with a clear correction message.