> ## Documentation Index
> Fetch the complete documentation index at: https://flywheel.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# MCP tool reference

> Every tool the Flywheel MCP server offers, the permission it needs and its parameters

The Flywheel MCP server offers 112 tools. A connection only sees the tools its permissions cover (see [Permissions](/mcp/overview#permissions-and-which-tools-an-agent-sees)). Tools marked destructive either remove data or reach real people; an agent should confirm with the person it works for before calling them. Connected agents can search this catalog with `search_docs` and `kind: "tool"`.

## People, properties and segments

### search\_people

**Permission:** `people.read` · **Effect:** read-only

Search people by any identifier value (email, phone, external id, Stripe id, a full\_name identifier): a case-insensitive substring match on identifier values only, so the first\_name / last\_name fields are not searched. The fastest way to resolve a person when you have something a human typed. Returns up to 50 person summaries, unordered; use get\_person for full detail.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `query` | `string` | yes | Free-text search: a name, email, phone number, or external id |

### list\_people

**Permission:** `people.read` · **Effect:** read-only

List people with exact filters (subscription status, MRR/LTV ranges, health band, segment membership, identified-only, and what they did: did\_event / did\_not\_event) and pagination. Event conditions query a cohort without saving a segment, e.g. did\_event \[\{event:'signed\_up', within\_days:30}] with did\_not\_event \[\{event:'subscription\_started'}]. For free-text lookup use search\_people instead.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `subscription_status` | `string` | no | incomplete \| incomplete\_expired \| trialing \| active \| past\_due \| canceled \| unpaid \| paused \| inactive |
| `payment_processor` | `string` | no | stripe \| paddle \| whop |
| `health_band` | `string` | no | critical \| low \| moderate \| healthy \| power |
| `external_user_id` | `string` | no | Exact match on the org's own user id |
| `email` | `string` | no | Exact match on primary email |
| `identified` | `boolean` | no | true = only people with an email or name |
| `segment_id` | `string` | no | Only members of this segment (from list\_segments) |
| `mrr_gte` | `number` | no | |
| `mrr_lte` | `number` | no | |
| `ltv_gte` | `number` | no | |
| `ltv_lte` | `number` | no | |
| `created_after` | `string` | no | ISO 8601 |
| `created_before` | `string` | no | ISO 8601 |
| `did_event` | `object[]` | no | Only people who did each of these events (all must match). Same shape as an event segment filter. |
| `did_not_event` | `object[]` | no | Only people who did NOT do each of these events in the window (all time if no window). At most 5 event conditions across did\_event and did\_not\_event. |
| `sort` | `string` | no | created\_at (default) \| mrr \| ltv \| first\_name \| last\_name \| primary\_email \| last\_opened\_at \| last\_clicked\_at |
| `order` | `string` | no | asc \| desc (default desc) |
| `limit` | `integer` | no | Page size, default 50 |
| `offset` | `integer` | no | |

### get\_person

**Permission:** `people.read` · **Effect:** read-only

Fetch one person in full: names, primary contact details, subscription standing (status, MRR, LTV, payment dates), engagement recency, custom properties, contact methods, and assigned team members.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `person_id` | `string` | yes | The person UUID, from search\_people / list\_people / identify\_person |

### identify\_person

**Permission:** `people.write` · **Effect:** changes data

Find or create a person by any identifier (email, phone, external user id, Stripe customer id, anonymous id), through the same pipeline integrations use, including duplicate-contact merging. Idempotent: repeating the call returns the same person. Start here when you are not sure a person exists yet. Resolves by external\_user\_id first, then stripe\_customer\_id, email, anonymous\_id, phone; at least one is required (a name alone cannot identify anyone). external\_user\_id must be the org's real user id for this person, read from their product, never a guess: it is attached as an identifier and never replaces the id the person already has.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `email` | `string` | no | |
| `phone` | `string` | no | |
| `external_user_id` | `string` | no | The org's own user id for this person |
| `stripe_customer_id` | `string` | no | |
| `anonymous_id` | `string` | no | |
| `first_name` | `string` | no | |
| `last_name` | `string` | no | |
| `custom_properties` | `object` | no | key → value (string, number or boolean), applied a moment after the person resolves. Every key must already have a property config (list\_property\_configs); an unknown key is refused and nothing is written — create it first with create\_property\_config. Values are coerced to the config’s type; invalid values and reserved keys are dropped silently. For strict typing use set\_person\_property; for many people use create\_import. |

### delete\_person

**Permission:** `people.delete` · **Effect:** changes data, destructive or reaches people

Permanently delete one person and everything attached to them: their events, sent-email history, subscriptions, webinar registrations and signals. It cannot be undone. A person with an opt-out or a bounce on record is refused, because their suppression would go with them and they would come back mailable if identified again; it is also refused while tasks or connection invites reference the person. Resolve the person first and confirm with the person you are working for before calling it. Every call is recorded with this connection's credential.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `person_id` | `string` | yes | The person UUID, from search\_people / list\_people / identify\_person |
| `reason` | `string` | no | Why, recorded with the deletion (for example a cleanup run id). At most 200 characters. |

### list\_identifiers

**Permission:** `people.read` · **Effect:** read-only

List every identifier attached to a person (emails, phones, external ids, Slack ids …) with primary/contact-method flags.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `person_id` | `string` | yes | |

### add\_identifier

**Permission:** `people.write` · **Effect:** changes data

Attach an identifier to a person. Conflict-safe: if the value already belongs to another person the result reports the conflict instead of silently duplicating. Emails are lowercased.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `person_id` | `string` | yes | |
| `identifier_type` | `string` | yes | email \| phone \| user\_id \| org\_assigned\_user\_id \| stripe\_customer\_id \| anonymous\_id \| slack\_user\_id \| slack\_channel \| telegram \| full\_name |
| `identifier_value` | `string` | yes | |
| `is_contact_method` | `boolean` | no | true if messages may be sent to this identifier. Defaults to false: an email added without true is not mailable. |
| `is_primary_for_type` | `boolean` | no | true to make this the primary identifier of its type |

### update\_identifier

**Permission:** `people.write` · **Effect:** changes data

Edit an identifier: its value or its contact-method / primary flags. Promoting to primary demotes the previous primary of that type.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `person_id` | `string` | yes | |
| `identifier_id` | `string` | yes | From list\_identifiers |
| `identifier_value` | `string` | no | |
| `is_contact_method` | `boolean` | no | |
| `is_primary_for_type` | `boolean` | no | |

### remove\_identifier

**Permission:** `people.write` · **Effect:** changes data, destructive or reaches people

Delete one identifier from a person. The person remains; only this way of reaching or recognizing them goes.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `person_id` | `string` | yes | |
| `identifier_id` | `string` | yes | |

### list\_property\_configs

**Permission:** `people.read` · **Effect:** read-only

The org's custom property definitions (key, name, type). Values and segment filters both reference these keys — call this before set\_person\_property or create\_segment with custom properties.

No parameters.

### create\_property\_config

**Permission:** `people.write` · **Effect:** changes data

Define a new custom property (key + display name + type). Do this once per property; then set per-person values with set\_person\_property.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `key` | `string` | yes | snake\_case key, letters/digits/underscores only |
| `name` | `string` | yes | Display name shown in the dashboard |
| `type` | `string` | yes | string \| number \| boolean (store dates as ISO strings) |
| `description` | `string` | no | |

### update\_property\_config

**Permission:** `people.write` · **Effect:** changes data

Rename a custom property or edit its description. Key and type are immutable — existing values and segment filters depend on them.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `config_id` | `string` | yes | |
| `name` | `string` | no | |
| `description` | `string` | no | |

### delete\_property\_config

**Permission:** `people.write` · **Effect:** changes data, destructive or reaches people

Delete a custom property definition AND every per-person value stored under it. Irreversible for the stored values — read it back first if in doubt.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `config_id` | `string` | yes | |

### get\_person\_properties

**Permission:** `people.read` · **Effect:** read-only

The custom property values set on one person, keyed by property key.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `person_id` | `string` | yes | |

### set\_person\_property

**Permission:** `people.write` · **Effect:** changes data

Set (or overwrite) one custom property value on a person. The key must already have a property config and the value must match its type — create the config first if it doesn't exist.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `person_id` | `string` | yes | |
| `key` | `string` | yes | A property config key, from list\_property\_configs |
| `value` | `string \| number \| boolean \| null` | yes | null clears the value while keeping the row |

### delete\_person\_property

**Permission:** `people.write` · **Effect:** changes data, destructive or reaches people

Remove one custom property value from a person entirely (the property config itself stays).

| Parameter | Type | Required | Description |
| - | - | - | - |
| `person_id` | `string` | yes | |
| `key` | `string` | yes | |

### list\_segments

**Permission:** `people.read` · **Effect:** read-only

Every saved segment (dynamic filtered group of people) with its conditions in public form. System segments are read-only.

No parameters.

### get\_segment

**Permission:** `people.read` · **Effect:** read-only

One segment with its filter conditions.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `segment_id` | `string` | yes | |

### list\_segment\_members

**Permission:** `people.read` · **Effect:** read-only

The people currently matching a segment, newest first, evaluated live by the same engine the dashboard and broadcast sends use. `total` is the full member count; page with limit/offset. For segment members that also match other filters, use list\_people with segment\_id.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `segment_id` | `string` | yes | |
| `limit` | `integer` | no | Page size, default 50 |
| `offset` | `integer` | no | |

### create\_segment

**Permission:** `people.write` · **Effect:** changes data

Create a segment from filter conditions (ANDed). Filters reference built-in person fields, custom property keys (call list\_property\_configs first), or events a person did ("signed\_up at least twice in the last 30 days", "never invoice\_paid"). Date and event-window values may be relative tokens (now-30d) that stay rolling. The result shows the conditions as stored; verify membership with list\_segment\_members.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `name` | `string` | yes | |
| `description` | `string` | no | |
| `color` | `string` | no | Hex color for the dashboard pill, e.g. #6366f1 |
| `filters` | `object[]` | yes | All conditions must match (AND) |

### update\_segment

**Permission:** `people.write` · **Effect:** changes data, destructive or reaches people

Edit a segment. Passing filters REPLACES the whole condition set — send the complete list you want, not a delta. System segments cannot be edited.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `segment_id` | `string` | yes | |
| `name` | `string` | no | |
| `description` | `string` | no | |
| `color` | `string` | no | |
| `filters` | `object[]` | no | |

### delete\_segment

**Permission:** `people.write` · **Effect:** changes data, destructive or reaches people

Delete a segment definition. People are untouched — only the saved grouping goes. System segments cannot be deleted.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `segment_id` | `string` | yes | |

### list\_subscriptions

**Permission:** `people.read` · **Effect:** read-only

A person's subscriptions as mirrored from the payment processor: items, cost, status, period and trial dates. Read-only.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `person_id` | `string` | yes | |

### get\_opt\_outs

**Permission:** `people.read` · **Effect:** read-only

A person's marketing opt-out status for email and SMS. No record means fully opted in.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `person_id` | `string` | yes | |

### set\_opt\_outs

**Permission:** `people.write` · **Effect:** changes data

Set marketing opt-outs for a person. Opting someone out stops marketing email/SMS to them — honor requests immediately. Opting back IN should only follow explicit consent from the person.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `person_id` | `string` | yes | |
| `email_opt_out` | `boolean` | no | |
| `sms_opt_out` | `boolean` | no | |

### list\_data\_imports

**Permission:** `people.read` · **Effect:** read-only

CSV data-import history, newest first: status, file name, total\_rows and processed\_rows (successes and failures combined).

No parameters.

### create\_import

**Permission:** `people.write` · **Effect:** changes data

Start a people import (contacts from a CSV, spreadsheet or another tool). Parse the file yourself and send its column names plus up to 20 sample rows (objects keyed by column). Creates a draft — nothing is written to people yet — and returns, per column, what the values look like and a suggested target when the header and values agree, plus every valid target (person fields, consent columns, existing properties). Next: set\_import\_mapping.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `name` | `string` | yes | A name for the import, e.g. the file name |
| `columns` | `string[]` | yes | Every column in the file, exactly as named in the rows (max 100) |
| `sample_rows` | `object[]` | no | Up to 20 representative rows, used to check the mapping |

### set\_import\_mapping

**Permission:** `people.write` · **Effect:** changes data

Decide what every column of a draft import becomes. Every column must be mapped or "skip". A target is a person field (email, alternate\_email, phone, alternate\_phone, external\_user\_id, stripe\_customer\_id, anonymous\_id, first\_name, last\_name), a consent column (subscribed\_to\_emails or unsubscribed\_from\_emails — these only ever opt people OUT), \{ "property": "\<existing key>" }, or \{ "new\_property": \{ "key": "snake\_case", "name": "...", "type": "string|number|boolean" } } to declare a new one. At least one identifier is required. Refuses unknown property keys (with the closest match), system keys, two columns on one target, and sample values that do not fit. Returns issues and the first sample rows as the person each becomes. Only saved when there are no errors.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `import_id` | `string` | yes | |
| `mapping` | `object` | yes | column → target, for every column |
| `discard_rows` | `boolean` | no | Required to change the mapping after rows were appended: drops them so they can be re-appended under the new mapping |

### append\_import\_rows

**Permission:** `people.write` · **Effect:** changes data

Add rows to a draft import after its mapping is set, in file order, up to 500 per call (50000 per import). Each row is an object keyed by column. Every row is checked against the mapping; rows that do not fit are rejected with the row number and reason and are not imported. Blank cells are ignored, never written over existing values. Call once per chunk, one call at a time.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `import_id` | `string` | yes | |
| `rows` | `object[]` | yes | |

### preview\_import

**Permission:** `people.read` · **Effect:** read-only

Dry run of a draft import — writes nothing. Says how many people it will create and update, which existing property values change, how many people get opted out, which new properties get created, and which rows would MERGE two or more existing people into one (a merge deletes all but one and cannot be undone). merges: "block" (default) refuses to produce a token while merges exist; "skip\_rows" leaves those rows out; "allow" merges them. Returns a commit\_token when ready. Show this preview to the person you are working for before committing.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `import_id` | `string` | yes | |
| `merges` | `"block" \| "skip_rows" \| "allow"` | no | |

### commit\_import

**Permission:** `people.write` · **Effect:** changes data, destructive or reaches people

Run a previewed import with its commit\_token. Re-checks everything and refuses if anything changed since the preview (re-preview then). Creates the declared new properties, then queues the rows; people are created and updated over the next minutes. Only after the person you are working for has seen preview\_import and said go. Follow it with get\_import.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `import_id` | `string` | yes | |
| `commit_token` | `string` | yes | |

### get\_import

**Permission:** `people.read` · **Effect:** read-only

One import's status, mapping and counts (staged, succeeded, failed), with the reason for each failed row (latest 100).

| Parameter | Type | Required | Description |
| - | - | - | - |
| `import_id` | `string` | yes | |

### cancel\_import

**Permission:** `people.write` · **Effect:** changes data

Delete a draft import and its staged rows. Only drafts: a committed import cannot be cancelled.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `import_id` | `string` | yes | |

## events

### track\_event

**Permission:** `events.ingest` · **Effect:** changes data, destructive or reaches people

Record events that people did (1–100 per call), synchronously: each comes back with a status — inserted, duplicate (that event\_id is already stored; a safe retry), event\_id\_conflict (another organization uses that id; pick another), invalid (refused before anything was written; see error) or failed (retry with the same event\_id) — plus the stored row id and the person it landed on. An inserted event can carry a warning (stored, but starting its workflows failed): do not re-send it; tell the person you work for. THIS CAN REACH REAL PEOPLE: every live workflow triggered on the event name enrolls the person, even when the timestamp is in the past. Before backfilling history, check which live workflows listen on that name (list\_workflows / get\_workflow) and confirm with the person you work for. Name each person one way: person\_id (resolve with search\_people first), or user\_id / email / anonymous\_id. An unknown person is refused unless create\_person: true. track\_event never changes a person’s identifiers or properties — use identify\_person / set\_person\_property for that. Always pass a stable event\_id so retries cannot double-record. At most 5 event names the org has never recorded may be introduced per call (new\_event\_names lists them) — a new name creates a new event type, so check spelling against list\_events first. Limit: 600 events per minute per organization.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `events` | `object[]` | yes | The events to record. One event is a one-element array. |

### list\_events

**Permission:** `events.read` · **Effect:** read-only

Read the org's events, newest first, a page at a time (next\_cursor). With person\_id: that person's history, ordered by when each event happened. Without it: the org's feed, ordered by when Flywheel received each event, and limited to events received in the last 7 days (before until, if given) unless you pass since — the response's received\_since says where that window starts. Filter by event name(s), a time window on when events happened (since / until), or how they arrived (location: fw\_api, fw\_mcp, fw\_pixel, …). The pixel's autocapture events (\$ac\_\*) are left out unless you name one or pass include\_autocapture. Large properties come back cut down (properties\_truncated); pass include\_properties: false to skip them. Use it to check what names an org records before track\_event, to confirm an event landed, or to see what a person did.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `person_id` | `string` | no | Only this person (UUID from search\_people / list\_people / identify\_person) |
| `event` | `string` | no | Only events with this name |
| `events` | `string[]` | no | Only events with any of these names |
| `since` | `string` | no | Only events that happened at or after this ISO-8601 instant |
| `until` | `string` | no | Only events that happened before this ISO-8601 instant |
| `location` | `string` | no | Only events that arrived this way: fw\_api \| fw\_mcp \| fw\_pixel \| fw\_automation \| segment \| posthog \| … |
| `include_autocapture` | `boolean` | no | Include the pixel’s \$ac\_\* autocapture events when no name is given. Default false. |
| `include_properties` | `boolean` | no | Default true. |
| `cursor` | `string` | no | next\_cursor from the previous page, with the same filters |
| `limit` | `integer` | no | Page size, default 25 |

## Workflows

### list\_workflows

**Permission:** `workflows.read` · **Effect:** read-only

List every workflow (automation) in the organization: id, name, status (live / paused / null for never live), published version.

No parameters.

### get\_workflow

**Permission:** `workflows.read` · **Effect:** read-only

Fetch one workflow: `definition` is the published version (what runs), `draft` the current draft (what the builder shows and what publish\_workflow publishes), both in the database-row form the write tools take, with node labels. has\_unpublished\_changes says whether the draft would run differently; draft\_placeholders lists unfilled "Add step" nodes that block publishing; draft\_hash fingerprints the draft for update\_workflow\_node. Email bodies make this large: view "summary" replaces each body (template\_content, template\_content\_json, html, editor JSON) with \{ omitted: true, bytes } and keeps subject, preview text, template id and variables — use it unless you are changing a body, and include "draft" or "definition" to get one half. A summary is for reading: never send it back through update\_workflow (the write refuses omitted bodies); change single steps with update\_workflow\_node, or read view "full" before editing bodies.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `automation_id` | `string` | yes | The workflow (automation) UUID, from list\_workflows |
| `view` | `"full" \| "summary"` | no | "full" (default): every param. "summary": message bodies replaced by \{ omitted, bytes } |
| `include` | `"both" \| "draft" \| "definition"` | no | "both" (default), or only the draft or only the published definition |

### list\_workflow\_runs

**Permission:** `workflows.read` · **Effect:** read-only

List runs of a workflow, newest first, with `total` (every run matching the filter, not just this page). status filters by run status; "stuck" lists in\_progress runs with no activity for 30 minutes (no log, no step moving, no wait still to end, including a webinar wait whose session moved later), each with last\_activity\_at and last\_node\_id (where it stopped) — the ones to look at with get\_run and then cancel\_run / retry\_run.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `automation_id` | `string` | yes | The workflow (automation) UUID |
| `status` | `"in_progress" \| "completed" \| "failed" \| "cancelled" \| "stuck"` | no | in\_progress \| completed \| failed \| cancelled \| stuck |
| `limit` | `integer` | no | Page size, default 50 |
| `offset` | `integer` | no | Rows to skip, default 0 |

### get\_run

**Permission:** `workflows.read` · **Effect:** read-only

Fetch one workflow run: its status and attempt, step-by-step logs (each with node\_id / node\_type, the node's id as get\_workflow and retry\_run name it), and `steps`, the run's step ledger (which steps were dispatched, are running, are waiting and until when, finished or failed). include\_context: false leaves out the run context, which can be tens of KB.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `run_id` | `string` | yes | The run UUID, from list\_workflow\_runs |
| `include_context` | `boolean` | no | Include the run context. Default true. |

### get\_node\_types

**Permission:** `workflows.read` · **Effect:** read-only

The workflow engine's node-type catalog: every trigger, action, and exit-criteria type with JSON Schemas for its params (per version), the params paths that may hold a \{\{context.\<source>.\<path>}} reference (context\_ref\_fields, per version; a reference anywhere else is refused with FIELD\_NOT\_EDITABLE), its `next` wiring shape, and what it contributes to run context. Generated from the same schemas validation enforces. Call this FIRST, before building or editing a workflow definition; never use a type marked executable: false.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `types` | `string[]` | no | Only return these node types (e.g. \["send\_email", "if\_else"]). Omit for the full catalog. |

### get\_vocabulary

**Permission:** `workflows.read` · **Effect:** read-only

The organization's referenceable vocabulary: event names, custom property configs, email/SMS sender ids, health score rule ids, webinar ids, team member user ids, and email template ids. Workflow params referencing ids outside this vocabulary fail validation — read it before writing params, never invent ids. Keys: eventNames, customPropertyConfigs (id, name, key), senderConfigIds, smsSenderIds, healthScoreRuleIds, webinarIds, webinarSeriesIds, orgMemberUserIds, templateIds. Pixel autocapture events (\$ac\_\*) are left out of event names to keep the list readable; they are still valid triggers.

No parameters.

### get\_node\_context

**Permission:** `workflows.read` · **Effect:** read-only

For one node of a saved workflow (its draft first, else its published version): which upstream nodes' context exists when it runs and the JSON Schema of what each contributes, plus the always-available root context. Use this to pick real \{\{context.\<source\_id>.\<path>}} references instead of guessing.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `automation_id` | `string` | yes | The workflow (automation) UUID |
| `node_id` | `string` | yes | trigger\_id / action\_id / exit\_criteria\_id of the node whose available context you want |

### validate\_workflow

**Permission:** `workflows.read` · **Effect:** read-only

Dry-run validation of a workflow definition in database-row form. Runs all three tiers (structure + schemas, context references, org vocabulary) and returns structured findings with stable codes. Never writes. It does NOT run the save-time checks create/update/publish run first (lowercase UUID ids, builder-drawable conditions, trigger\_condition must be null), so valid: true can still be refused at save. Pass exit\_criteria: \[] when there are none. create\_workflow / update\_workflow return the same findings for what they save, so this is for checking a definition before saving it.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `triggers` | `object[]` | yes | org\_automation\_triggers rows: \{ trigger\_id, trigger\_type, trigger\_version, params, next } |
| `actions` | `object[]` | yes | org\_automation\_actions rows: \{ action\_id, action\_type, action\_version, params, next, exit\_gate\_id } |
| `exit_criteria` | `object[]` | yes | org\_automation\_exit\_criteria rows: \{ exit\_criteria\_id, params, exit\_next } |

### create\_workflow

**Permission:** `workflows.write` · **Effect:** changes data

Create a workflow as a DRAFT, laid out on the builder canvas so a person can open it. Order of work: get\_node\_types (params + next shape of each type) → get\_vocabulary (ids and event names params may use) → this. The draft saves even when validation finds problems: `findings` lists them, and errors block publish\_workflow, not the save — fix them with update\_workflow. saved: false means the graph could not be drawn at all (bad ids, unknown types, broken wiring); nothing was created. Pass automation\_id (a UUID you generate) to make retries return the same workflow instead of a duplicate. Nothing runs until publish\_workflow and then set\_workflow\_status live.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `name` | `string` | yes | Workflow name shown in the dashboard |
| `definition` | `object` | no | Omit for an empty draft |
| `automation_id` | `string` | no | Optional id (lowercase UUID) for idempotent creation |

### update\_workflow

**Permission:** `workflows.write` · **Effect:** changes data, destructive or reaches people

Rename a workflow and/or REPLACE its draft. definition is the whole graph, not a patch: read it with get\_workflow (draft, or definition when there is no draft), change it, send all of it back. Nodes that keep their id keep their labels. A canvas this API laid out that nobody has rearranged is re-laid out on every save. A canvas a person arranged (moved a node or resized an exit criteria box), or one saved before auto layout existed, keeps its positions and new nodes are fitted around them; pass relayout: true once to tidy such a canvas. Returns the saved workflow and the draft's findings. The published version keeps running unchanged until publish\_workflow. If a person has the workflow open in the builder, their next edit there can overwrite a draft saved here — re-read before publishing.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `automation_id` | `string` | yes | The workflow (automation) UUID |
| `name` | `string` | no | |
| `definition` | `object` | no | The workflow in database-row form (the shape get\_workflow returns). Every node id is a fresh lowercase v4 UUID you generate, reused verbatim wherever the node is referenced. next wires the graph: next\_action\_id / true\_action\_id / false\_action\_id / drop-off branches name ACTION ids. End a run with \{ next\_type: "completed" }; the string "completed" as a branch target means an unconnected branch (a warning, or an error for node types that cannot resolve it). name/description label the node on the canvas. |
| `relayout` | `boolean` | no | Lay the whole canvas out afresh, even one a person arranged or one saved before auto layout existed (those otherwise keep their positions) |

### update\_workflow\_node

**Permission:** `workflows.write` · **Effect:** changes data, destructive or reaches people

Change ONE node of the draft in place — its params and/or its canvas name and description — without sending the whole graph back. Positions, wiring, unfilled "Add step" nodes and every other node stay exactly as they are (the one exception: changing a drop\_off trigger's params.config re-draws its branch edges). Pass params (the whole new params object) or params\_patch (an RFC 7396 merge patch: objects merge key by key, null deletes a key, arrays and values replace), not both. The write only lands on the draft it read: if the draft changes while the edit is being made (a builder save, another write), nothing is saved and you get failed\_precondition — re-read and apply it again. Pass expected\_draft\_hash (draft\_hash from get\_workflow) to also refuse when the draft changed since YOUR read. saved: false means the node would not be representable on the canvas — nothing was written and findings say why. Otherwise returns the workflow and the draft's validation findings; errors block publish\_workflow, not the save. The published version keeps running unchanged until publish\_workflow. Wiring changes (next, adding or removing nodes) still go through update\_workflow.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `automation_id` | `string` | yes | The workflow (automation) UUID |
| `node_id` | `string` | yes | trigger\_id / action\_id / exit\_criteria\_id of the node to change, from get\_workflow |
| `params` | `object` | no | The node's whole new params object (shape per type from get\_node\_types) |
| `params_patch` | `object` | no | RFC 7396 merge patch over the current params, e.g. \{"subject": "New subject"} or \{"content": \{"variables": \{"var\_1": \{"fallback": "there"}}}}; null removes a key |
| `name` | `string` | no | The node's canvas label ('' clears it) |
| `description` | `string` | no | The node's canvas description ('' clears it); not for exit criteria |
| `expected_draft_hash` | `string` | no | draft\_hash from get\_workflow; refuses if the draft has changed since |

### send\_test\_email

**Permission:** `workflows.publish` · **Effect:** changes data, destructive or reaches people

Send ONE email step of a workflow (send\_email / send\_marketing\_email) as a real email to up to 5 of this organization's team members — never to anyone else — to check how it reads before publishing. It is rendered exactly as a run would render it: variables are filled from person\_id's data when given (a sample person from search\_people), and anything without a value uses the step's fallback text; fallback\_used lists those \{\{context.\*}} paths (with no person, every person variable falls back). The subject is prefixed "\[Test] ". Uses the draft's version of the step when the draft has it, else the published one (source picks explicitly). No run is created and nothing is recorded as sent, so it never affects who later receives the step. It is a real send through the organization's sender and counts toward the plan's email allowance. At most 20 test emails per organization per hour, each recipient counted (resource\_exhausted past that).

| Parameter | Type | Required | Description |
| - | - | - | - |
| `automation_id` | `string` | yes | The workflow (automation) UUID |
| `node_id` | `string` | yes | action\_id of the send\_email / send\_marketing\_email step, from get\_workflow |
| `to` | `string[]` | yes | Recipients: team members' login emails only |
| `person_id` | `string` | no | A person (from search\_people) whose data fills the variables; omit to see the fallbacks |
| `source` | `"draft" \| "published"` | no | Which version of the step; default the draft when it has the step |

### publish\_workflow

**Permission:** `workflows.publish` · **Effect:** changes data, destructive or reaches people

Publish the draft — or `definition`, which is saved as the draft first — as the workflow's next version. Full validation runs; any error blocks the publish and comes back in findings (published: false). Pass expected\_version (the version you read) so you never publish over someone else's publish. A LIVE workflow runs the new version on its next trigger; a workflow that is not live still needs set\_workflow\_status live to run at all.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `automation_id` | `string` | yes | The workflow (automation) UUID |
| `definition` | `object` | no | Publish this instead of the saved draft (it becomes the draft) |
| `expected_version` | `integer` | no | The version you last read; refuses if it has moved |

### set\_workflow\_status

**Permission:** `workflows.publish` · **Effect:** changes data

Turn a workflow live or pause it. Live means its published version runs on every new trigger — real messages to real people — so confirm with the person you are working for first. Requires a published version and an active subscription. Pausing stops new runs.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `automation_id` | `string` | yes | The workflow (automation) UUID |
| `status` | `"live" \| "paused"` | yes | |

### delete\_workflow

**Permission:** `workflows.write` · **Effect:** changes data, destructive or reaches people

Permanently delete a workflow together with its published versions, run history and sent-message history. Cannot be undone. A live workflow is refused — pause it first. To experiment safely, clone\_workflow instead.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `automation_id` | `string` | yes | The workflow (automation) UUID |

### cancel\_run

**Permission:** `workflows.publish` · **Effect:** changes data, destructive or reaches people

Stop one run: an in\_progress (or failed) run becomes cancelled and runs no further steps. Steps already dispatched and waits already queued are not chased — each checks the run before it runs and stops. Idempotent: an already cancelled run comes back with changed: false, and a completed run is left completed. Pausing a workflow (set\_workflow\_status) stops NEW runs; this stops one that is under way. A cancelled run can be resumed with retry\_run.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `run_id` | `string` | yes | The run UUID, from list\_workflow\_runs |
| `reason` | `string` | no | Why, recorded on the run |

### retry\_run

**Permission:** `workflows.publish` · **Effect:** changes data, destructive or reaches people

Re-run a failed or cancelled run from a node, as a new attempt, on the workflow version the run originally ran on (not the current draft or published version). from\_action\_id defaults to where the run stopped (the node of its last error, or the step a cancel interrupted). Steps from the old attempt still in flight are skipped. Steps run asynchronously: this returns once the first step is dispatched — follow it with get\_run. Marketing and transactional emails a person already got from a node are not sent again; SMS, webhooks, Slack and Intercom steps are: if any between from\_action\_id and the end already ran on this run, the call is refused with the list unless confirm\_repeat\_side\_effects is true. Idempotent: a concurrent second retry dispatches nothing (changed: false). An in\_progress run that is stuck must be cancel\_run'd first.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `run_id` | `string` | yes | The run UUID, from list\_workflow\_runs |
| `from_action_id` | `string` | no | The node to run from: an action\_id from get\_workflow (or node\_id from get\_run's logs). Default: where the run stopped. |
| `confirm_repeat_side_effects` | `boolean` | no | Set true only after confirming with the person you work for that the listed steps may reach people or systems a second time |

### clone\_workflow

**Permission:** `workflows.write` · **Effect:** changes data

Copy a workflow — its draft, or its published canvas when it has no draft — into a new DRAFT workflow. Node ids are regenerated and every reference to them (wiring, exit gates, \{\{context.\<id>}} templates) follows. The copy is never live. Good for trying changes without touching a running workflow.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `automation_id` | `string` | yes | The workflow to copy |
| `name` | `string` | no | Defaults to "Copy of \<name>" |
| `new_automation_id` | `string` | no | Optional id (lowercase UUID) for the copy, for idempotent retries |

## Webinars

### list\_webinars

**Permission:** `webinars.read` · **Effect:** read-only

List webinar sessions with filters and paging: status, series, a start-time window. For "what is coming up" pass starts\_after = now and order = asc. Returns session summaries; use get\_webinar for counts of registrants and messages.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `status` | `string` | no | draft \| scheduled \| live \| completed \| canceled |
| `series_id` | `string` | no | Only sessions a series built |
| `starts_after` | `string` | no | Only sessions starting at or after this ISO-8601 instant |
| `starts_before` | `string` | no | Only sessions starting before this ISO-8601 instant |
| `order` | `"asc" \| "desc"` | no | By start time; default desc (latest first) |
| `limit` | `integer` | no | Page size, default 50 |
| `offset` | `integer` | no | Rows to skip, default 0 |

### get\_webinar

**Permission:** `webinars.read` · **Effect:** read-only

Fetch one webinar session: its schedule, status, platform link, series, registration counts (registered / attended / no-show / unresolved) and its own messages by status. Use get\_webinar\_plan for what those messages and bound sequences send and when.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `webinar_id` | `string` | yes | The webinar session UUID, from list\_webinars |

### create\_webinar

**Permission:** `webinars.write` · **Effect:** changes data

Create a one-off webinar session. Pass webinar\_id (a v4 UUID you generate) to make it safe to retry: the same id returns the session already made (created: false). For a recurring webinar use create\_webinar\_series instead. Then link it to its platform (link\_webinar\_platform) and check what registrants will receive with get\_webinar\_plan.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `webinar_id` | `string` | no | Optional caller-chosen v4 UUID for the new session — the idempotency key |
| `title` | `string` | yes | The session title |
| `start_time` | `string` | yes | When it starts — ISO-8601 with an offset, e.g. 2026-10-06T19:00:00-04:00 |
| `timezone` | `string` | no | IANA timezone the session is presented in, e.g. America/New\_York |
| `duration_minutes` | `integer` | no | How long it runs; "after it ends" messages count from start + duration |
| `join_url` | `string` | no | Where attendees join (or register on the platform) |
| `status` | `"draft" \| "scheduled"` | no | Default scheduled |
| `automation_id` | `string` | no | Optional workflow to label the session with (from list\_workflows); informational only |

### update\_webinar

**Permission:** `webinars.write` · **Effect:** changes data, destructive or reaches people

Change a webinar session. Omitted fields stay as they are; null clears timezone, duration\_minutes, join\_url or automation\_id. A new start\_time (a reschedule) or duration\_minutes moves every pending reminder, the session’s own messages and the platform attendance pull with it — the response says what moved. status moves: draft → scheduled | canceled; scheduled → live | completed | canceled; live → completed | canceled; canceled → scheduled (restore; messages whose time passed while canceled are not re-sent); completed is final. Canceling stops the session’s own messages and refuses new registrations and attendance; sequence and workflow runs registrants are already in keep going. Linked platforms are not rescheduled — change the time on the platform too. Confirm with the person you work for before canceling or rescheduling a session people registered for.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `webinar_id` | `string` | yes | The webinar session UUID, from list\_webinars |
| `title` | `string` | no | |
| `start_time` | `string` | no | The new start — ISO-8601 with an offset, e.g. 2026-10-06T19:00:00-04:00 |
| `timezone` | `string \| null` | no | IANA timezone, or null to clear |
| `duration_minutes` | `integer \| null` | no | |
| `join_url` | `string \| null` | no | |
| `status` | `"draft" \| "scheduled" \| "live" \| "completed" \| "canceled"` | no | |
| `automation_id` | `string \| null` | no | A workflow id to label the session with, or null |

### delete\_webinar

**Permission:** `webinars.write` · **Effect:** changes data, destructive or reaches people

Delete a webinar session that never gathered anything: no registrations, no messages sent, not part of a series and not a series’ template. Anything else is refused with the reason — cancel it instead (update\_webinar status canceled), which keeps the history; for a series session, cancel it or skip its date on the series.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `webinar_id` | `string` | yes | The webinar session UUID, from list\_webinars |

### clone\_webinar

**Permission:** `webinars.write` · **Effect:** changes data

Copy a webinar session to a new start time: title, duration, join link and its messages (as drafts, re-anchored to the new start — nothing sends until scheduled). Registrations and the platform link are not copied; link the clone with link\_webinar\_platform. Pass new\_webinar\_id (a v4 UUID) to make a retry safe.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `webinar_id` | `string` | yes | The webinar session UUID, from list\_webinars |
| `start_time` | `string` | yes | When the copy starts — ISO-8601 with an offset, e.g. 2026-10-06T19:00:00-04:00 |
| `title` | `string` | no | Title for the copy; default the source’s |
| `new_webinar_id` | `string` | no | Optional caller-chosen v4 UUID for the copy |

### list\_webinar\_registrants

**Permission:** `webinars.read` · **Effect:** read-only

Who registered for a webinar session, with what the session knows about each: attended or not, watch time where the platform reports it, left early, offer clicks, purchase and refund. segment narrows to one audience; segment\_counts always covers everyone.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `webinar_id` | `string` | yes | The webinar session UUID, from list\_webinars |
| `segment` | `string` | no | registered \| attended \| no\_show \| left\_early \| clicked\_offer \| purchased \| refunded |
| `limit` | `integer` | no | Page size, default 100 |
| `offset` | `integer` | no | Rows to skip, default 0 |

### register\_for\_webinar

**Permission:** `webinars.write` · **Effect:** changes data, destructive or reaches people

Register a person for a webinar session. This sends real messages: it enrolls them into every live workflow and sequence bound to the session (confirmations, reminders) and pushes them to the linked platform for their join link. Resolve the person first (search\_people, or identify\_person to create them). Returns join\_url, the link this person joins with: their own when the platform issued one (join\_url\_personal: true), otherwise the session’s shared link. Idempotent: registering someone twice changes nothing and sends nothing (already\_registered: true, with the same join\_url). A canceled session refuses registrations. Confirm with the person you work for before registering real people.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `webinar_id` | `string` | yes | The webinar session UUID, from list\_webinars |
| `person_id` | `string` | yes | The person UUID, from search\_people / list\_people / identify\_person |

### set\_webinar\_attendance

**Permission:** `webinars.write` · **Effect:** changes data, destructive or reaches people

Record whether a registered person attended a webinar session (attended: false marks a no-show). This arms the session’s outcome follow-ups — the attended and no-show sequences — so it sends real messages after the session. Linked platforms (Demio, WebinarJam, Zoom) report attendance on their own; use this for sessions without one, or to correct a record.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `webinar_id` | `string` | yes | The webinar session UUID, from list\_webinars |
| `person_id` | `string` | yes | The person UUID, from search\_people / list\_people / identify\_person |
| `attended` | `boolean` | yes | |

### get\_webinar\_plan

**Permission:** `webinars.read` · **Effect:** read-only

Everything a webinar session sends and when: its own clock-anchored messages, the workflows and sequences bound to it (registration and outcome rails), and — for each entry time — the exact, timestamped list someone registering at that moment receives, with late steps skipped as the runtime skips them. With no entry\_times it samples ten days out, three days out, two hours out and after the end. Check this before and after changing a session’s messages or schedule.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `webinar_id` | `string` | yes | The webinar session UUID, from list\_webinars |
| `entry_times` | `string[]` | no | ISO-8601 registration moments to resolve (at most 12) |

### get\_webinar\_revenue

**Permission:** `webinars.read` · **Effect:** read-only

What a webinar session earned: registered and attended counts, clicks, buyers, and revenue split into total, attributed to its messages, and unattributed — with a per-message breakdown. reconciles: false means the figures do not add up; report that rather than the numbers.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `webinar_id` | `string` | yes | The webinar session UUID, from list\_webinars |

### link\_webinar\_platform

**Permission:** `webinars.write` · **Effect:** changes data

Link a webinar session to the platform it runs on, so registrations are pushed there (personal join links) and attendance comes back after it ends. demio: event\_id is the Demio event, date\_id the session inside it; by default the session’s title, start, duration, timezone and registration link are pulled from Demio (sync\_details: false keeps yours) — a moved start re-anchors reminders. webinarjam: event\_id is the WebinarJam webinar id, date\_id its schedule id — leave it out and the schedule on the session’s date is picked; the operator must also add the registration / attended / missed automation rules on the WebinarJam side. zoom: event\_id is the Zoom webinar id (spaces fine); it must exist on the connected Zoom account, and a one-off Zoom webinar can back only one session. Linking a series’ template session to a recurring Zoom webinar (recurring: true) also links every upcoming session of that series to it, and each session the series builds from then on. The response names what was linked (topic) so you can confirm it. Demio and Zoom must be connected in Settings → Integrations; WebinarJam links without an API key but then cannot pick the schedule or push registrations. A recurring WebinarJam webinar shares one schedule id across every date, so it cannot back a series: use one WebinarJam schedule per date.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `webinar_id` | `string` | yes | The webinar session UUID, from list\_webinars |
| `platform` | `"demio" \| "webinarjam" \| "zoom"` | yes | |
| `event_id` | `string` | yes | The platform’s event / webinar id |
| `date_id` | `string` | no | demio / webinarjam: the session or schedule id within it |
| `sync_details` | `boolean` | no | demio only: pull the session details from Demio (default true) |

### unlink\_webinar\_platform

**Permission:** `webinars.write` · **Effect:** changes data, destructive or reaches people

Disconnect a webinar session from its platform (Demio, WebinarJam or Zoom): new registrations stop being pushed there and the post-session attendance pull is cancelled. Registrations and attendance already recorded stay. Unlinking a series’ template session from Zoom also unlinks the series’ upcoming sessions that shared that Zoom webinar.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `webinar_id` | `string` | yes | The webinar session UUID, from list\_webinars |

### list\_webinar\_series

**Permission:** `webinars.read` · **Effect:** read-only

List webinar series — repeat rules that keep sessions built ahead on a cadence — with cadence, status and template session.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `status` | `string` | no | active \| paused \| archived |

### get\_webinar\_series

**Permission:** `webinars.read` · **Effect:** read-only

Fetch one webinar series: cadence, the sessions it has built, skipped dates, and upcoming\_unbuilt — the next dates it has not built yet, which is what skip\_webinar\_occurrence takes.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `series_id` | `string` | yes | The webinar series UUID, from list\_webinar\_series |

### create\_webinar\_series

**Permission:** `webinars.write` · **Effect:** changes data

Create a recurring webinar: the first session (on the first cadence slot after now) becomes the template, and the series keeps build\_ahead\_count sessions built ahead, each a copy of the template (title, duration, join link, messages — armed messages stay armed). Set up the template session’s messages, then new sessions inherit them — sessions built during this call copy the template as it is now (empty), so keep build\_ahead\_count low until the template is ready, or set those sessions’ messages up too. The platform link is inherited only where one link can serve every date: link the template to a recurring Zoom webinar and every upcoming session of the series is linked to it, including the ones already built; with WebinarJam each session is linked to the schedule on its own date; a one-off Zoom webinar or a Demio event stays the template’s alone, so link those sessions one by one. Pass series\_id (a v4 UUID) to make a retry safe.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `series_id` | `string` | no | Optional caller-chosen v4 UUID for the series — the idempotency key |
| `name` | `string` | yes | Series name; also the first session’s title |
| `cadence` | `object` | yes | |
| `timezone` | `string` | yes | IANA timezone the cadence runs in, e.g. America/New\_York — 7pm stays 7pm across DST |
| `build_ahead_count` | `integer` | no | Sessions kept built and open for sign-ups; default 3 |
| `duration_minutes` | `integer` | yes | How long each session runs |
| `join_url` | `string` | no | |

### update\_webinar\_series

**Permission:** `webinars.write` · **Effect:** changes data, destructive or reaches people

Change a webinar series’ rule — name, cadence, timezone, build\_ahead\_count, or status (active builds; paused and archived build nothing). Applies to sessions not built yet; existing sessions keep their times (reschedule one with update\_webinar). What each new session copies comes from the template session, so edit that session to change future ones. The series re-checks its horizon immediately.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `series_id` | `string` | yes | The webinar series UUID, from list\_webinar\_series |
| `name` | `string` | no | |
| `cadence` | `object` | no | |
| `timezone` | `string` | no | |
| `build_ahead_count` | `integer` | no | |
| `status` | `"active" \| "paused" \| "archived"` | no | |

### materialize\_webinar\_series

**Permission:** `webinars.write` · **Effect:** changes data

Build any sessions a webinar series is short of right now, instead of at its next scheduled check. Safe to repeat — it only fills gaps.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `series_id` | `string` | yes | The webinar series UUID, from list\_webinar\_series |

### skip\_webinar\_occurrence

**Permission:** `webinars.write` · **Effect:** changes data

Stop a webinar series from building one upcoming date (take it from get\_webinar\_series upcoming\_unbuilt). A date that already has a session is not skipped here — cancel that session with update\_webinar instead.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `series_id` | `string` | yes | The webinar series UUID, from list\_webinar\_series |
| `occurrence_date` | `string` | yes | The local date to skip, yyyy-MM-dd |
| `reason` | `string` | no | A note for the team, e.g. "holiday" |

### unskip\_webinar\_occurrence

**Permission:** `webinars.write` · **Effect:** changes data

Put a skipped date back on a webinar series. If it falls inside the build-ahead horizon its session is built immediately.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `series_id` | `string` | yes | The webinar series UUID, from list\_webinar\_series |
| `occurrence_date` | `string` | yes | The skipped local date, yyyy-MM-dd |

### get\_webinar\_series\_revenue

**Permission:** `webinars.read` · **Effect:** read-only

What a webinar series earned across its sessions, deduplicated (one purchase attributed to two sessions counts once), with a per-session breakdown of the most recent sessions. The per-session figures do not sum to the series total by design.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `series_id` | `string` | yes | The webinar series UUID, from list\_webinar\_series |
| `session_limit` | `integer` | no | How many recent sessions to break out; default 50 |

## Broadcasts

### list\_broadcasts

**Permission:** `broadcasts.read` · **Effect:** read-only

List broadcasts, newest first, with filters and paging: status (draft | scheduled | sending | sent | stopped | error), channel (email | sms), webinar\_id. Returns summaries with sent / opened / clicked counts; get\_broadcast has the body.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `status` | `string` | no | draft \| scheduled \| sending \| sent \| stopped \| error |
| `channel` | `"email" \| "sms"` | no | |
| `webinar_id` | `string` | no | Only the broadcasts of one webinar session |
| `limit` | `integer` | no | Page size, default 50 |
| `offset` | `integer` | no | Rows to skip, default 0 |

### get\_broadcast

**Permission:** `broadcasts.read` · **Effect:** read-only

Fetch one broadcast: subject, preview text and HTML body (or the text), its variables, audience, timing, sender and counts — in the form update\_broadcast takes, so edit and send back.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `broadcast_id` | `string` | yes | The broadcast UUID, from list\_broadcasts or create\_broadcast |

### list\_broadcast\_variables

**Permission:** `broadcasts.read` · **Effect:** read-only

What a broadcast's variables can read: the recipient's fields, the organization's custom properties, and — for a broadcast tied to a webinar — the session (title, start time, the recipient's join link, calendar links). Each entry's `variable` goes straight into a variable definition.

No parameters.

### create\_broadcast

**Permission:** `broadcasts.write` · **Effect:** changes data

Create a broadcast draft. Nothing is sent: next plan\_broadcast, then schedule\_broadcast. audience is required: everyone (every person with an email address), segment (with segment\_id), or webinar (with webinar\_id — the session's registrants, narrowed by webinar\_audience, optionally timed from the session with webinar\_offset\_minutes). Email needs subject, html and sender\_id before it can be scheduled; a text broadcast (channel sms, webinars only) needs message\_body. Personalize with variables (list\_broadcast\_variables). Pass broadcast\_id (a v4 UUID you generate) to make the call safe to retry.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `broadcast_id` | `string` | no | Optional caller-chosen v4 UUID — the idempotency key |
| `channel` | `"email" \| "sms"` | no | email (default) or sms — texts go to webinar registrants only |
| `audience` | `"everyone" \| "segment" \| "webinar"` | yes | Who it goes to |
| `segment_id` | `string \| null` | no | audience segment: the segment UUID (list\_segments) |
| `webinar_id` | `string \| null` | no | audience webinar: the session UUID (list\_webinars) |
| `webinar_audience` | `"registered" \| "attended" \| "no_show"` | no | Webinar only: everyone registered (default), those who attended, or no-shows |
| `webinar_offset_minutes` | `integer \| null` | no | Webinar only: time it from the session — minutes before the anchor (negative = after). 60 = an hour before; null = pick a send\_at at plan time instead |
| `webinar_anchor` | `"start" \| "end"` | no | Webinar only: count the offset from the session start (default) or end |
| `subject` | `string` | no | Email subject — sent exactly as written (no variables) |
| `preview_text` | `string \| null` | no | Email: the inbox preview line (no variables) |
| `html` | `string` | no | Email body HTML. Keep it simple — p, h1–h3, strong, em, a, ul/ol/li, img, hr, br — which is what the dashboard editor reopens; the org's email layout (logo, footer, unsubscribe) wraps it at send. Put \{\{var\_\<id>}} where a value goes in text; a link whose URL is a variable is \<a href="\{\{link\_var\_\<id>}}"> (the whole href). Define every id in variables. |
| `message_body` | `string` | no | Text broadcasts only: the message, with \{\{var\_\<id>}} where a value goes (defined in variables). Plain text — no links markup; put a URL variable in the text. |
| `variables` | `object \| null` | no | Flywheel variables — the same system the dashboard editor saves. Map each placeholder id to what fills it: \{"var\_first": \{"variable": "\{\{context.root\_context.org\_user.first\_name}}", "fallback": "there"}}. Ids are var\_\<letters/digits/*/-> (link\_var*… for a link). Paths come from list\_broadcast\_variables; webinar paths only on a webinar broadcast. Variables the body does not use are dropped. |
| `sender_id` | `string \| null` | no | Email: who it is from — a sender id from list\_senders |

### update\_broadcast

**Permission:** `broadcasts.write` · **Effect:** changes data

Change a broadcast draft. Omitted fields stay as they are; null clears preview\_text, sender\_id or webinar\_offset\_minutes. html / message\_body replace the whole body (read it with get\_broadcast first); variables replace the whole variable map — the body and its variables are checked together. Within the same audience kind, stored ids and timing are kept unless you send new ones; switching kind needs the new kind’s id. channel cannot be changed. A scheduled broadcast has to be unscheduled first; a stopped or errored one returns to draft. Any change invalidates an earlier plan — plan again.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `broadcast_id` | `string` | yes | The broadcast UUID, from list\_broadcasts or create\_broadcast |
| `audience` | `"everyone" \| "segment" \| "webinar"` | no | |
| `segment_id` | `string \| null` | no | audience segment: the segment UUID (list\_segments) |
| `webinar_id` | `string \| null` | no | audience webinar: the session UUID (list\_webinars) |
| `webinar_audience` | `"registered" \| "attended" \| "no_show"` | no | Webinar only: everyone registered (default), those who attended, or no-shows |
| `webinar_offset_minutes` | `integer \| null` | no | Webinar only: time it from the session — minutes before the anchor (negative = after). 60 = an hour before; null = pick a send\_at at plan time instead |
| `webinar_anchor` | `"start" \| "end"` | no | Webinar only: count the offset from the session start (default) or end |
| `subject` | `string` | no | Email subject — sent exactly as written (no variables) |
| `preview_text` | `string \| null` | no | Email: the inbox preview line (no variables) |
| `html` | `string` | no | Email body HTML. Keep it simple — p, h1–h3, strong, em, a, ul/ol/li, img, hr, br — which is what the dashboard editor reopens; the org's email layout (logo, footer, unsubscribe) wraps it at send. Put \{\{var\_\<id>}} where a value goes in text; a link whose URL is a variable is \<a href="\{\{link\_var\_\<id>}}"> (the whole href). Define every id in variables. |
| `message_body` | `string` | no | Text broadcasts only: the message, with \{\{var\_\<id>}} where a value goes (defined in variables). Plain text — no links markup; put a URL variable in the text. |
| `variables` | `object \| null` | no | Flywheel variables — the same system the dashboard editor saves. Map each placeholder id to what fills it: \{"var\_first": \{"variable": "\{\{context.root\_context.org\_user.first\_name}}", "fallback": "there"}}. Ids are var\_\<letters/digits/*/-> (link\_var*… for a link). Paths come from list\_broadcast\_variables; webinar paths only on a webinar broadcast. Variables the body does not use are dropped. |
| `sender_id` | `string \| null` | no | Email: who it is from — a sender id from list\_senders |

### delete\_broadcast

**Permission:** `broadcasts.write` · **Effect:** changes data, destructive or reaches people

Delete a broadcast nobody has received any of (a draft, or a stopped / errored one that never went out). A scheduled one has to be unscheduled first; one that went out stays for its history.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `broadcast_id` | `string` | yes | The broadcast UUID, from list\_broadcasts or create\_broadcast |

### plan\_broadcast

**Permission:** `broadcasts.read` · **Effect:** read-only

Show exactly what scheduling a broadcast would do, without scheduling it: how many people it reaches (counted the way the sender counts), when it goes out and how long it takes, who it is from, the body as one real recipient would get it, and checks — errors block, warnings are worth a look. Pass send\_at (ISO-8601 with an offset, at least an hour from now; plan 90+ minutes out so the plan survives review, since the hour is checked again at scheduling). Omit send\_at for a webinar broadcast timed from its session (webinar\_offset\_minutes set): passing one is an error. When nothing blocks, returns a plan\_token for schedule\_broadcast (good for an hour, and only while the broadcast stays as planned). Show the plan to the person you work for before scheduling.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `broadcast_id` | `string` | yes | The broadcast UUID, from list\_broadcasts or create\_broadcast |
| `send_at` | `string` | no | When it should go out — ISO-8601 with an offset, at least an hour from now, e.g. 2026-10-06T15:00:00-04:00 |

### schedule\_broadcast

**Permission:** `broadcasts.send` · **Effect:** changes data, destructive or reaches people

Schedule a broadcast to go out to real people, as planned: pass the plan\_token from plan\_broadcast. It goes out at the planned time (never sooner than an hour from now) and is refused if anything changed since the plan, the plan is over an hour old, or the send time is now less than an hour away — plan again then. The plan's checks also run again: anything that now blocks (sender domain unverified, session canceled or started, allowance used up) refuses it, and edits to the target segment's filters or a rescheduled session count as changes. Only call this after the person you work for has seen the plan and agreed. Until it goes out, unschedule\_broadcast (or Unschedule in the dashboard) pulls it back.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `broadcast_id` | `string` | yes | The broadcast UUID, from list\_broadcasts or create\_broadcast |
| `plan_token` | `string` | yes | The plan\_token plan\_broadcast returned |

### unschedule\_broadcast

**Permission:** `broadcasts.write` · **Effect:** changes data

Take a scheduled broadcast back to draft so nothing goes out. Refused once sending has begun.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `broadcast_id` | `string` | yes | The broadcast UUID, from list\_broadcasts or create\_broadcast |

### get\_broadcast\_stats

**Permission:** `broadcasts.read` · **Effect:** read-only

How a broadcast did: sent / opened / clicked totals and per day (email), and where every queued recipient ended up — sent, still queued, failed, or skipped (and why: opted out, bounced, complained, duplicate, allowance used up, webinar already started).

| Parameter | Type | Required | Description |
| - | - | - | - |
| `broadcast_id` | `string` | yes | The broadcast UUID, from list\_broadcasts or create\_broadcast |

## senders

### list\_senders

**Permission:** `senders.read` · **Effect:** read-only

The email senders a broadcast or workflow email can go out from (use the id as sender\_id / sender\_config\_id) and whether each is ready — a sender needs a verified sending domain. team\_member marks a teammate's own sender (made when they joined) as opposed to an organization sender. Also whether the organization has an SMS number for text broadcasts.

No parameters.

### list\_sending\_domains

**Permission:** `senders.read` · **Effect:** read-only

The organization's sending domains — what can follow the @ in a sender's address. ready = verified, so create\_sender can use it; an unverified domain lists the DNS records still missing (adding a domain and verifying it happens in the dashboard, Settings → Email). sender\_count is how many senders use each.

No parameters.

### create\_sender

**Permission:** `senders.write` · **Effect:** changes data

Create an organization email sender: from\_name plus \<username>@\<domain> on one of the organization's verified sending domains (list\_sending\_domains). Nothing is sent — use the returned id as a broadcast's sender\_id or a workflow email's sender\_config\_id. Refused for a domain that is not this organization's or not verified yet. Safe to retry: if a sender already sends from that address it is returned unchanged (created: false; change it with update\_sender), and a sender\_id you generate makes the call idempotent.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `sender_id` | `string` | no | Optional v4 UUID you generate, making the call safe to retry |
| `from_name` | `string` | yes | The name recipients see, e.g. "Jane at Acme". No quotes, \< > or line breaks. |
| `username` | `string` | yes | The part of the address before the @, e.g. "hello" or "jane". Letters, digits and . \_ + -; lowercased. The domain comes from domain\_id. |
| `domain_id` | `string` | yes | The sending domain UUID, from list\_sending\_domains — one of this organization's verified (ready) domains |
| `reply_to` | `string \| null` | no | Where replies go. Omit or null = the from address itself. |
| `full_name` | `string \| null` | no | Signature: the person's full name |
| `job_title` | `string \| null` | no | Signature: their job title |
| `url` | `string \| null` | no | Signature: a link (http/https). On create, omitted = the organization's website; "" = no link. |
| `signature_enabled` | `boolean` | no | Append the signature block to emails from this sender (default false) |
| `include_flywheel_signature` | `boolean` | no | Add a "sent with Flywheel" line to the signature (default false) |

### update\_sender

**Permission:** `senders.write` · **Effect:** changes data, destructive or reaches people

Change a sender. Omitted fields stay as they are; reply\_to null = replies go to the from address. A new username or domain\_id is checked like create\_sender (own verified domain, valid username) and refused if another sender already uses the resulting address. Takes effect at the next send everywhere the sender is used: the result counts the workflows and sequences (live, or with runs still in progress), scheduled broadcasts and published transactional templates that send from it and lists warnings (agents that name the sender are not counted) — tell the person you are working for before changing a sender that is in use. A sender cannot move to another team member or organization.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `sender_id` | `string` | yes | The sender UUID, from list\_senders or create\_sender |
| `from_name` | `string` | no | The name recipients see, e.g. "Jane at Acme". No quotes, \< > or line breaks. |
| `username` | `string` | no | The part of the address before the @, e.g. "hello" or "jane". Letters, digits and . \_ + -; lowercased. The domain comes from domain\_id. |
| `domain_id` | `string` | no | The sending domain UUID, from list\_sending\_domains — one of this organization's verified (ready) domains |
| `reply_to` | `string \| null` | no | Where replies go. Omit or null = the from address itself. |
| `full_name` | `string \| null` | no | Signature: the person's full name |
| `job_title` | `string \| null` | no | Signature: their job title |
| `url` | `string \| null` | no | Signature: a link (http/https); "" or null = no link |
| `signature_enabled` | `boolean` | no | Append the signature block to emails from this sender |
| `include_flywheel_signature` | `boolean` | no | Add a "sent with Flywheel" line to the signature |

## Signals

### list\_signal\_rules

**Permission:** `signals.read` · **Effect:** read-only

List the event rules: each turns a matching event into a signal (a person flagged for action), with its routing (human or agent, rung), dedupe, expiry, resolve\_on and follow\_up.

No parameters.

### get\_signal\_rule

**Permission:** `signals.read` · **Effect:** read-only

Fetch one event rule by id.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `rule_id` | `string` | yes | |

### create\_signal\_rule

**Permission:** `signals.write` · **Effect:** changes data

Create an event rule, or replace one whole when key matches an existing rule (safe to retry). Takes effect within a minute. A rule with handler agent and rung auto starts that agent on every new signal (capped per agent per day); every new signal also emits \$fw\_signal\_created for playbook workflows. To change one field of an existing rule use update\_signal\_rule.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `key` | `string \| null` | no | Stable name for the rule ("E1"). Posting the same key again replaces that rule, so creation is safe to retry |
| `label` | `string \| null` | no | Human name shown in the dashboard; defaults to the bucket |
| `bucket` | `string` | yes | The queue bucket the signal lands in, e.g. "expansion" or "payment\_failing" |
| `enabled` | `boolean` | no | Default true |
| `on_event` | `string` | yes | The event name that fires the rule, exactly as it arrives (e.g. hit\_limit, entered\_stage, \$fw\_stripe\_invoice\_payment\_failed) |
| `filters` | `object[]` | no | AND-ed filters over the event properties. Filters see EVENT properties only |
| `occurrence` | `object \| null` | no | "The 3rd time within 14 days". Omit or null to fire on every occurrence |
| `handler` | `"human" \| "agent"` | no | Who works the signal. Default human |
| `handler_agent_id` | `string \| null` | no | Required when handler is agent (list\_signal\_agents) |
| `rung` | `"draft" \| "approve" \| "auto"` | no | Autonomy. auto + handler agent dispatches the agent the moment the signal is queued. Default draft |
| `default_assignee_user_id` | `string \| null` | no | Team member the signal is assigned to |
| `queue` | `boolean` | no | false = match but write nothing. Default true |
| `expiry_minutes` | `integer` | no | How long the signal is worth working. Default 4320 (72h); 10080 = 7 days |
| `dedupe` | `object` | no | What counts as the same signal. Default one per person per calendar day |
| `purchase_resolves` | `boolean` | no | A succeeded purchase closes the signal as won |
| `resolve_on` | `object[]` | no | Events that close this rule's open signals as won when they arrive for the same person after the signal opened |
| `follow_up` | `object \| null` | no | Hand a still-unresolved signal to an agent after a delay. Must be shorter than expiry\_minutes |
| `contact_reason` | `string \| null` | no | The "why contact them" line on the signal |
| `fact_properties` | `string[]` | no | Event properties copied onto the signal (and onto \$fw\_signal\_created as fact\_\<name>) |

### update\_signal\_rule

**Permission:** `signals.write` · **Effect:** changes data

Change some fields of an event rule. Omitted fields keep their value; null clears any nullable field (follow\_up, handler\_agent\_id, occurrence, key, label, default\_assignee\_user\_id, contact\_reason). The merged rule is re-validated whole.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `rule_id` | `string` | yes | |
| `key` | `string \| null` | no | Stable name for the rule ("E1"). Posting the same key again replaces that rule, so creation is safe to retry |
| `label` | `string \| null` | no | Human name shown in the dashboard; defaults to the bucket |
| `bucket` | `string` | no | The queue bucket the signal lands in, e.g. "expansion" or "payment\_failing" |
| `enabled` | `boolean` | no | Default true |
| `on_event` | `string` | no | The event name that fires the rule, exactly as it arrives (e.g. hit\_limit, entered\_stage, \$fw\_stripe\_invoice\_payment\_failed) |
| `filters` | `object[]` | no | AND-ed filters over the event properties. Filters see EVENT properties only |
| `occurrence` | `object \| null` | no | "The 3rd time within 14 days". Omit or null to fire on every occurrence |
| `handler` | `"human" \| "agent"` | no | Who works the signal. Default human |
| `handler_agent_id` | `string \| null` | no | Required when handler is agent (list\_signal\_agents) |
| `rung` | `"draft" \| "approve" \| "auto"` | no | Autonomy. auto + handler agent dispatches the agent the moment the signal is queued. Default draft |
| `default_assignee_user_id` | `string \| null` | no | Team member the signal is assigned to |
| `queue` | `boolean` | no | false = match but write nothing. Default true |
| `expiry_minutes` | `integer` | no | How long the signal is worth working. Default 4320 (72h); 10080 = 7 days |
| `dedupe` | `object` | no | What counts as the same signal. Default one per person per calendar day |
| `purchase_resolves` | `boolean` | no | A succeeded purchase closes the signal as won |
| `resolve_on` | `object[]` | no | Events that close this rule's open signals as won when they arrive for the same person after the signal opened |
| `follow_up` | `object \| null` | no | Hand a still-unresolved signal to an agent after a delay. Must be shorter than expiry\_minutes |
| `contact_reason` | `string \| null` | no | The "why contact them" line on the signal |
| `fact_properties` | `string[]` | no | Event properties copied onto the signal (and onto \$fw\_signal\_created as fact\_\<name>) |

### delete\_signal\_rule

**Permission:** `signals.write` · **Effect:** changes data, destructive or reaches people

Delete an event rule. Signals it already wrote keep their history.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `rule_id` | `string` | yes | |

### list\_review\_triggers

**Permission:** `signals.read` · **Effect:** read-only

List the triggers that summon the AI reviewer to look at a person when an event happens.

No parameters.

### create\_review\_trigger

**Permission:** `signals.write` · **Effect:** changes data

Summon the AI reviewer when an event (plus filters, optional occurrence count) happens, after a delay, optionally with a focus question.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `label` | `string` | no | |
| `on_event` | `string` | yes | The event that summons the reviewer |
| `property_filters` | `object[]` | no | |
| `occurrence` | `object \| null` | no | "The 3rd time within 14 days". Omit or null to fire on every occurrence |
| `delay_minutes` | `integer` | no | Review this long after the event. 0 = now; 720 = their first 12 hours |
| `focus` | `string \| null` | no | A question for the reviewer to answer about the person |
| `priority` | `"normal" \| "critical"` | no | |
| `enabled` | `boolean` | no | |

### update\_review\_trigger

**Permission:** `signals.write` · **Effect:** changes data

Change some fields of a review trigger. Omitted fields keep their value.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `trigger_id` | `string` | yes | |
| `label` | `string` | no | |
| `on_event` | `string` | no | |
| `property_filters` | `object[]` | no | |
| `occurrence` | `object \| null` | no | "The 3rd time within 14 days". Omit or null to fire on every occurrence |
| `delay_minutes` | `integer` | no | Review this long after the event. 0 = now; 720 = their first 12 hours |
| `focus` | `string \| null` | no | A question for the reviewer to answer about the person |
| `priority` | `"normal" \| "critical"` | no | |
| `enabled` | `boolean` | no | |

### delete\_review\_trigger

**Permission:** `signals.write` · **Effect:** changes data, destructive or reaches people

Delete a review trigger.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `trigger_id` | `string` | yes | |

### list\_signal\_actions

**Permission:** `signals.read` · **Effect:** read-only

List the AI reviewer's action catalog: the verbs it may answer with (human\_reachout, agent\_draft, alert, watch, none, plus custom ones) and how each one's signals are routed.

No parameters.

### create\_signal\_action

**Permission:** `signals.write` · **Effect:** changes data

Add a verb to the reviewer's action catalog. rung\_ceiling is fixed at creation and can never be raised.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `key` | `string` | yes | lowercase\_with\_underscores |
| `label` | `string` | yes | |
| `guidance` | `string` | no | Goes into the reviewer's prompt verbatim: when to use this action |
| `delivery` | `"signal" \| "watch" \| "none"` | yes | |
| `rung` | `"draft" \| "approve" \| "auto"` | no | Never above the action's rung\_ceiling |
| `enabled` | `boolean` | no | |
| `handler` | `"human" \| "agent"` | no | |
| `handler_agent_id` | `string \| null` | no | The agent that works signals born from this action |
| `default_assignee_user_id` | `string \| null` | no | |
| `follow_up` | `object \| null` | no | Hand a still-unresolved signal to an agent after a delay. Must be shorter than expiry\_minutes |
| `rung_ceiling` | `"draft" \| "approve" \| "auto"` | no | Default auto |

### update\_signal\_action

**Permission:** `signals.write` · **Effect:** changes data

Change how a reviewer action is routed or guided: e.g. route agent\_draft to an agent at rung auto. Omitted fields keep their value. rung cannot exceed the action's rung\_ceiling.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `action_id` | `string` | yes | |
| `label` | `string` | no | |
| `guidance` | `string` | no | Goes into the reviewer's prompt verbatim: when to use this action |
| `delivery` | `"signal" \| "watch" \| "none"` | no | |
| `rung` | `"draft" \| "approve" \| "auto"` | no | Never above the action's rung\_ceiling |
| `enabled` | `boolean` | no | |
| `handler` | `"human" \| "agent"` | no | |
| `handler_agent_id` | `string \| null` | no | The agent that works signals born from this action |
| `default_assignee_user_id` | `string \| null` | no | |
| `follow_up` | `object \| null` | no | Hand a still-unresolved signal to an agent after a delay. Must be shorter than expiry\_minutes |

### delete\_signal\_action

**Permission:** `signals.write` · **Effect:** changes data, destructive or reaches people

Delete a verb from the reviewer's catalog. Permanent: a deleted shipped verb is not re-seeded.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `action_id` | `string` | yes | |

### list\_event\_dictionary

**Permission:** `signals.read` · **Effect:** read-only

What each event means to the AI reviewer: description, salience (critical | notable | routine | noise, which decides what the reviewer sees), category, expected\_next, and whether it counts as activity.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `query` | `string` | no | Case-insensitive substring of the event name |
| `limit` | `integer` | no | Default 200 |

### update\_event\_dictionary

**Permission:** `signals.write` · **Effect:** changes data

Classify an event the org has already sent, as an operator: salience, category, description, expected\_next. Operator edits outrank AI and stats classification. Only events the org has already sent (anything else is not found). Salience critical books a review on every occurrence while the reviewer is on; noise removes the event from the reviewer's dossier.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `event_name` | `string` | yes | |
| `description` | `string` | no | |
| `salience` | `"critical" \| "notable" \| "routine" \| "noise"` | no | |
| `category` | `"error" \| "conversion" \| "engagement" \| "lifecycle" \| "billing" \| "system"` | no | |
| `expected_next` | `object \| null` | no | "This event usually leads to that one within N hours"; null clears |

### get\_reviewer\_settings

**Permission:** `signals.read` · **Effect:** read-only

The AI reviewer's settings (enabled, scan segment, org brief, debounce, staleness, max findings) and where the org stands against its review allowance this period.

No parameters.

### update\_reviewer\_settings

**Permission:** `signals.write` · **Effect:** changes data

Change the AI reviewer's settings. Omitted fields keep their value. scan\_segment\_id is stored and checked to be one of the org's segments, but the scan sweep does not apply it yet. Switching the reviewer on needs the AI Reviewer on the plan; check the allowance first, it spends money.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `reviewer_enabled` | `boolean` | no | |
| `scan_segment_id` | `string \| null` | no | |
| `org_brief` | `string \| null` | no | Read by the reviewer on every review: what the company sells, what activation means |
| `reviewer_debounce_minutes` | `integer` | no | |
| `reviewer_staleness_days` | `integer` | no | |
| `reviewer_max_findings` | `integer` | no | |

### list\_signal\_agents

**Permission:** `signals.read` · **Effect:** read-only

The org's agents that signals can be routed to, with each one's daily dispatch cap and how many signals it has been handed today (UTC).

No parameters.

### list\_signal\_producers

**Permission:** `signals.read` · **Effect:** read-only

The deterministic signal detectors (event\_rules, webinar\_session\_ended) and whether each is active or paused.

No parameters.

### set\_signal\_producer\_status

**Permission:** `signals.write` · **Effect:** changes data

Pause or resume a signal detector. A paused detector writes no signals.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `key` | `"event_rules" \| "webinar_session_ended"` | yes | |
| `status` | `"active" \| "paused"` | yes | |

## agents

### set\_signal\_agent\_cap

**Permission:** `agents.write` · **Effect:** changes data

Set how many signals one agent may be handed per UTC day (first dispatches and follow-ups together). Default 25. 0 stops dispatch to it. Signals past the cap wait for the next UTC day rather than being dropped.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `agent_id` | `string` | yes | |
| `signal_daily_dispatch_cap` | `integer` | yes | |

## Documentation

### search\_docs

**Permission:** `none (public)` · **Effect:** read-only

Search Flywheel's documentation and this server's tool catalog. Returns the best-matching doc sections (with the page path to pass to get\_doc) and tools (with the permission each needs). Use it before guessing how something works: workflow definition format and node params, validation finding codes, segment filters, broadcast variables and planning, webinar series, signals, connecting and permissions. Ask in plain words, e.g. 'if/else branch on a custom property' or 'why was my broadcast plan refused'.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `query` | `string` | yes | What you want to know, in plain words or with exact names (a tool, node type, finding code or field). |
| `limit` | `integer` | no | How many results to return (default 6, max 20). |
| `kind` | `"doc" \| "tool"` | no | Only return doc sections ('doc') or only tools ('tool'). Omit for both. |

### get\_doc

**Permission:** `none (public)` · **Effect:** read-only

Read one Flywheel documentation page as markdown — the whole page, or one section of it (a ## section includes its ### subsections). Paths come from search\_docs results (e.g. 'mcp/workflow-definitions'); a full docs URL works too. Start with 'mcp/overview' for the map of what the MCP can do and which page covers what.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `path` | `string` | yes | The page path from search\_docs (e.g. 'mcp/broadcasts'), or a docs URL. |
| `section` | `string` | no | Only this section: its heading text or anchor, from search\_docs or from the page’s `sections` list. Omit for the whole page. |

### list\_docs

**Permission:** `none (public)` · **Effect:** read-only

The table of contents of Flywheel's documentation: every page's path, title, one-line description and section headings, grouped as on the docs site. Use it to browse when search\_docs does not find what you need; read a page with get\_doc.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `area` | `string` | no | Only pages whose path or group starts with this, e.g. 'mcp', 'workflows', 'api-reference'. |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.