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.
The integration_call node calls an OAuth-backed third-party integration directly from inside a flow — no tools row required. The node carries the integration config (provider, account, action, args) on itself; the runtime resolves each entry in args_template per its declared mode (literal / ai_extract) — interpolating {{node.arg}} and {{metadata.key}} mustache tokens — and dispatches against the integration. The result routes deterministically the same way a tool-call node does (success / error / custom JSONPath branches, exactly one out-edge per fire).
Use an integration_call node when the action is a first-class capability of a managed integration — booking a calendar event, sending email — and you don’t want to hand-author a webhook URL + payload schema in the tools table for it.
For the /integrations connect / list / disconnect surface that owns the OAuth handshake, see the Integrations section.
Schema
{
"id": "book_appointment", // unique within flow_config.nodes[]
"type": "integration_call",
"name": "Book the appointment", // optional display label
"position": { "x": 480, "y": 240 }, // canvas position (UI-only)
"provider": "google_calendar", // 'google_calendar' | 'gmail' — locked at creation
"integration_id": "<uuid>", // FK to /integrations row; must be active in caller's company
"action": "create_event", // provider-scoped — see catalog below
"args_template": { // map of arg name → ArgValue (see "Arg modes" below)
"summary": "Consultation booking", // bare string = literal-mode shorthand
"start_time": { "mode": "ai_extract",
"description": "ISO-8601 start time the caller agreed on" },
"end_time": { "mode": "ai_extract",
"description": "ISO-8601 end time, default 30 minutes after start" }
},
"pre_fire_announcement": true,
"transitions": {
"success_next_step_id": "confirm_booked",
"error_next_step_id": "apologize_and_handoff",
"custom": [
{
"id": "no_avail",
"label": "No availability",
"jsonpath": "$.available",
"equals": "false",
"next_step_id": "suggest_alternatives"
}
]
}
}
| Field | Required | Notes |
|---|
id | yes | Unique within flow_config.nodes[]. |
type | yes | Literal "integration_call". |
name | no | Display label on the canvas. |
position | no | UI-only {x, y} on the React Flow canvas — the runtime ignores it. |
provider | yes | "google_calendar" or "gmail". Locked at node creation — you cannot flip Calendar to Gmail on an existing node; delete and recreate. |
integration_id | yes | UUID of an active row in the caller’s company integrations table whose provider matches this node’s provider. |
action | yes | Provider-scoped action — see catalog below. |
args_template | yes | Map of arg name → ArgValue. Each entry is one of: literal string (shorthand for {mode:"literal"}), {mode:"literal", value}, or {mode:"ai_extract", description}. Both value and description may contain {{node.arg}} / {{metadata.key}} mustache tokens — see Token interpolation below. Required keys depend on the chosen action — see catalog. |
pre_fire_announcement | no | Boolean toggle. When true, the platform plays a short platform-controlled hold tone the moment this node fires, so the caller doesn’t sit in silence while the action runs. Stops automatically when the action returns. Recommended for create_event / send_email / network-bound actions; skip for check_availability (which is fast). Tone is NOT configurable. |
timeout_secs | no | Per-node hard cap on execution time, in seconds (1–300). When the action doesn’t return in time the runtime cancels it and routes to error_next_step_id with tool_timeout_after_<N>s. Null/omitted = platform default (30s). |
transitions | yes | Same shape as a tool_call node: success_next_step_id, error_next_step_id, custom[] array. |
integration_call nodes do not support is_global. Like tool_call, they’re deterministic dispatch nodes; only conversation, transfer, and end nodes can be marked global.
Arg modes
Every entry in args_template is an ArgValue — a discriminated union with two writable shapes plus a string shorthand for literals:
| Mode | Shape | When to use |
|---|
literal (shorthand) | "some constant" | Constant value — bare string is shorthand for {mode:"literal", value:"some constant"}. May contain {{node.arg}} / {{metadata.key}} tokens. |
literal (explicit) | { "mode": "literal", "value": "..." } | Same as the shorthand, but unambiguous. value may contain tokens. |
ai_extract | { "mode": "ai_extract", "description": "..." } | The runtime fills the slot from the conversation right before the action fires. description is required, guides the extraction, and may itself contain tokens to splice prior context into the prompt. The agent asks the user for any missing ai_extract arg up to 3 times before routing to error_next_step_id — always wire an error branch when any arg uses this mode. |
Token interpolation
Both literal.value and ai_extract.description strings are scanned for mustache tokens at dispatch time. Two namespaces are supported:
{{<node_id>.<arg_name>}} — resolves to the value an earlier node AI-extracted from the conversation. Both integration_call AND tool_call source nodes are addressable. For an integration_call source the referenced arg must be declared in ai_extract mode in that node’s args_template. For a tool_call source the referenced arg name must match an entry in the linked tool’s config.payload_config.extraction_parameters — all extraction_parameters are AI-extracted at runtime, so any of them work as a token source. The referenced node must exist in the same flow.
{{metadata.<key>}} — resolves against per-call metadata. Built-in keys (always available): id, direction, agent_number, user_number, agent_name. User-defined keys come from the metadata dict passed at call dispatch (POST /calls body.metadata) — declare which custom keys your flow expects in flow_config.metadata.custom_metadata_keys so the dashboard can surface them.
Direction details for the built-in metadata keys: for inbound calls the caller is the user and the callee is the agent; for outbound the caller is the agent. agent_number / user_number hide that distinction so you don’t have to special-case direction.
Missing metadata.<key> resolves to an empty string — it is not an error. The caller is responsible for passing the value at call time. Save-time validation only catches dangling {{node.arg}} references where the referenced node or arg doesn’t exist or isn’t in ai_extract mode (args_template_dangling_reference).
Example — include the caller’s phone number in the email body so the support team can call back without context-switching, and reuse the recipient that an earlier node already extracted:
{
"id": "send_confirmation",
"type": "integration_call",
"provider": "gmail",
"integration_id": "1d4e2f3a-9c8b-4d6e-8f1a-7b2c3d4e5f6a",
"action": "send_email",
"args_template": {
"to": { "mode": "literal", "value": "{{create_event.attendees}}" },
"subject": "Your appointment is booked",
"body": { "mode": "ai_extract",
"description": "Friendly confirmation paragraph including the agreed time. Mention the callback number {{metadata.user_number}}." },
"cc": { "mode": "literal", "value": "{{metadata.user_number}}" }
},
"transitions": {
"success_next_step_id": "polite_end",
"error_next_step_id": "apologize_email_manually"
}
}
Example — pull a custom caller-supplied key ({{metadata.CustomerEmail}}) declared in flow_config.metadata.custom_metadata_keys:
{
"args_template": {
"to": { "mode": "literal", "value": "{{metadata.CustomerEmail}}" },
"subject": "Your appointment is booked",
"body": "We have you down for {{create_event.start_time}}."
}
}
If the caller forgets to pass CustomerEmail at dispatch time, the to field resolves to an empty string and the integration’s own validation will surface the issue at runtime via the node’s error branch.
Action catalog
The runtime knows the following actions per provider. Anything outside this catalog returns action_invalid at save time.
Google Calendar (provider: "google_calendar")
| Action | Required args | Optional args |
|---|
create_event | summary, start_time, end_time | attendees, description, location, calendar_id, time_zone |
list_events | — | time_min, time_max, max_results, query, calendar_id, time_zone |
check_availability | start_time, end_time | calendar_id, time_zone |
cancel_event | event_id | — |
start_time / end_time are ISO-8601 strings (e.g. "2026-05-12T10:00:00+02:00"). attendees is an array of email strings.
calendar_id accepts a Google calendar id or "primary" (default). cancel_event does not expose calendar_id — the runtime auto-resolves which calendar a given event lives on (tries primary first; falls back to scanning the user’s other writable calendars on a 404). One extra API call only when the event lives on a non-primary calendar.
time_zone is an IANA name (e.g. "Asia/Jerusalem"). When set, Google’s API response is pinned to that zone AND the event being created is stamped with it. When blank, the calendar’s default timezone is used. The dashboard ships an IANA picker; over the API any valid zone string works.
Response post-processing — wall-clock dateTimes
Calendar action responses (create_event, list_events, check_availability) are post-processed before the voice agent receives them, because Gemini Live’s ISO 8601 parser ignores offsets unreliably. The runtime:
- Strips the offset and seconds from each event’s
start.dateTime / end.dateTime, leaving wall-clock format ("2026-05-10 16:30").
- Removes the per-event
start.timeZone / end.timeZone fields (they document the authoring timezone — not the wall-clock zone — and contradict the wall-clock anchor).
- Adds top-level
timeZone + timeZone_note (“Event times below are wall-clock values in <tz> …”) so Live has one explicit anchor.
The <tz> quoted in the note is whatever you passed in time_zone, or — if blank — whatever timezone Google reported (the calendar’s primary). The agent never sees raw ISO offsets for these actions.
Concrete before/after for a list_events response in Israel (UTC+3):
// Google's raw payload (preserved as `raw_response_preview` on the call event)
{
"items": [
{
"id": "evt_abc123",
"summary": "Consultation",
"start": { "dateTime": "2026-05-10T16:30:00+03:00", "timeZone": "Asia/Jerusalem" },
"end": { "dateTime": "2026-05-10T17:00:00+03:00", "timeZone": "Asia/Jerusalem" }
}
]
}
// What the agent sees (sanitized — what `response_preview` carries)
{
"timeZone": "Asia/Jerusalem",
"timeZone_note": "Event times below are wall-clock values in Asia/Jerusalem (no offset needed).",
"items": [
{
"id": "evt_abc123",
"summary": "Consultation",
"start": { "dateTime": "2026-05-10 16:30" },
"end": { "dateTime": "2026-05-10 17:00" }
}
]
}
The raw, untouched Google response is preserved separately on the call event as raw_response_preview, viewable in the dashboard’s call-detail sheet alongside the agent-facing view (see “Tool result events” below). The flow agent itself only ever sees the sanitized version. JSONPath custom transitions (transitions.custom[].jsonpath) match against the sanitized view — so $.items[0].start.dateTime matches "2026-05-10 16:30", not Google’s ISO.
Gmail (provider: "gmail")
| Action | Required args | Optional args |
|---|
send_email | to, subject, body | html, cc, bcc |
to, cc, bcc accept either a single email string or an array of email strings. body is plain text; pass html: true to mark body as HTML.
Examples
Calendar — create an event
{
"id": "create_event",
"type": "integration_call",
"name": "Book the calendar event",
"provider": "google_calendar",
"integration_id": "8c2b1e1a-7c4d-4e1f-9a2b-3c4d5e6f7a8b",
"action": "create_event",
"args_template": {
"summary": { "mode": "ai_extract",
"description": "Caller's full name plus 'consultation'" },
"start_time": { "mode": "ai_extract",
"description": "ISO-8601 start time the caller agreed on" },
"end_time": { "mode": "ai_extract",
"description": "ISO-8601 end time, default 30 minutes after start" },
"attendees": { "mode": "ai_extract",
"description": "Caller's email address as a single-element array" },
"description": "Booked via inbound call"
},
"pre_fire_announcement": true,
"transitions": {
"success_next_step_id": "confirm_booked",
"error_next_step_id": "apologize_and_handoff"
}
}
Calendar — check availability with a custom branch
{
"id": "check_avail",
"type": "integration_call",
"name": "Check calendar availability",
"provider": "google_calendar",
"integration_id": "8c2b1e1a-7c4d-4e1f-9a2b-3c4d5e6f7a8b",
"action": "check_availability",
"args_template": {
"start_time": { "mode": "ai_extract",
"description": "ISO-8601 start time the caller proposed" },
"end_time": { "mode": "ai_extract",
"description": "ISO-8601 end time, default 30 minutes after start" }
},
"transitions": {
"success_next_step_id": "confirm_slot",
"error_next_step_id": "apologize_and_handoff",
"custom": [
{
"id": "busy",
"label": "Slot is taken",
"jsonpath": "$.available",
"equals": "false",
"next_step_id": "suggest_alternatives"
}
]
}
}
Gmail — send a confirmation, reusing the email captured earlier
The to address was already extracted by the create_event node above, so this node splices it through with a {{create_event.attendees}} token instead of asking the caller again:
{
"id": "send_confirmation",
"type": "integration_call",
"name": "Send confirmation email",
"provider": "gmail",
"integration_id": "1d4e2f3a-9c8b-4d6e-8f1a-7b2c3d4e5f6a",
"action": "send_email",
"args_template": {
"to": { "mode": "literal", "value": "{{create_event.attendees}}" },
"subject": "Your appointment is booked",
"body": { "mode": "ai_extract",
"description": "Friendly confirmation paragraph including the agreed time and a thank-you" }
},
"pre_fire_announcement": true,
"transitions": {
"success_next_step_id": "polite_end",
"error_next_step_id": "apologize_and_collect_email_manually"
}
}
Transitions
Routing on an integration_call node is deterministic — no LLM is involved. Exactly one out-edge fires per dispatch:
error_next_step_id fires only on hard failures: 4xx/5xx from the provider, network timeout, integration disconnected, integration belongs to a different company, missing required arg.
- Otherwise the dispatcher walks
custom[] top-to-bottom — first branch whose jsonpath extracts a value == equals (after JSON stringification) wins, loop returns, success is NOT also taken.
- If no custom matched →
success_next_step_id fires.
This is the same semantics as a tool_call node — see the flow composition guide for the full JSONPath subset and stringification rules. Always design an error branch.
The result of the action is injected as a <tool_result> block into the next node’s LLM context, so a single success → conversation node usually handles both happy-path and soft-fail cases gracefully via prompt instructions. Reach for custom[] only when the next node should be structurally different for that result shape.
Validation errors
Saves that include a malformed integration_call node return a 400 FLOW_INVALID response with one or more issues from this list:
| Code | When it fires |
|---|
schema_invalid | provider is not one of google_calendar / gmail (caught at zod parse). |
action_invalid | action is missing, empty, or not in the catalog for the chosen provider. |
integration_id_missing | integration_id is missing or empty. |
integration_not_in_company | The integration_id does not exist, belongs to a different company, has a non-active status, or its provider doesn’t match the node’s provider. |
success_not_wired | The node has no success_next_step_id and would dead-end on success. |
terminal_not_allowed | The node has no outgoing edges at all. Only end and transfer nodes may be terminal. |
unknown_target_node | A success / error / custom[].next_step_id references a node id that doesn’t exist in the flow. |
unreachable_node | The node exists but no path from the start node reaches it. |
args_template_missing_required | A required arg for the chosen action is absent from args_template, or present in literal mode with an empty value. |
args_template_missing_description | An arg in ai_extract mode is missing the description field. |
args_template_dangling_reference | A {{node.arg}} token references a node id that doesn’t exist in the flow, an arg that doesn’t exist on that node, or an arg that is not declared in ai_extract mode. (Note: {{metadata.key}} tokens are NOT validated at save time — missing values resolve to empty string at runtime.) |
The full save-validation contract — codes that apply to other node types too, the response shape, and the global terminal/reachability rules — lives on the agent update endpoint.
Setting up the integration
Before you can use an integration_call node, the integration itself has to exist:
- Connect Google Calendar / Gmail from the dashboard’s Integrations page. The OAuth handshake (popup → Google consent → callback) is dashboard-only; the public API does not expose a connect endpoint.
- List your integrations with
GET /integrations and grab the id of the row whose provider matches what you want to call.
- Plug that
id into your node’s integration_id.
If the integration is later disconnected, calls hitting that node fail with error (the runtime returns integration_disconnected) until you reconnect from the dashboard or update the node to point at a different integration_id.