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

# Integration-call node

> Call a connected third-party integration (Google Calendar, Gmail) directly from a flow node.

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](/api-reference/integrations/list) section.

## Schema

```jsonc theme={null}
{
  "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 (>0 and up to 600). 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:

```jsonc theme={null}
{
  "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`:

```jsonc theme={null}
{
  "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:

1. Strips the offset and seconds from each event's `start.dateTime` / `end.dateTime`, leaving wall-clock format (`"2026-05-10 16:30"`).
2. 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).
3. 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):

```jsonc theme={null}
// 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

```jsonc theme={null}
{
  "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

```jsonc theme={null}
{
  "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:

```jsonc theme={null}
{
  "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:

1. **`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.
2. 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.
3. 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](/api-reference/agents/update).

## Setting up the integration

Before you can use an `integration_call` node, the integration itself has to exist:

1. 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.
2. List your integrations with [`GET /integrations`](/api-reference/integrations/list) and grab the `id` of the row whose `provider` matches what you want to call.
3. 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`.
