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

# Signals

> Configure Flywheel Signals over MCP: event rules, the AI reviewer's triggers, actions and settings, the event dictionary, agent caps and signal producers.

A **signal** is a person flagged for action: someone your team (or one of your agents) should do something about, with the reason attached. Signals come from two places:

* **Event rules**: deterministic. "When `hit_limit` arrives with `limit = ai_credits`, open a signal." No AI involved.
* **The AI reviewer**: reads a per-person dossier (their event timeline filtered by the event dictionary, computed absences, your org brief) and answers with findings. Each finding recommends one action from your action catalog, and an admitted finding with a `signal` action becomes a signal.

Every signal carries its routing: who works it (a human or an agent), how much autonomy it has, and who it is assigned to. Signals are worked from the signals list in the Flywheel dashboard.

This page covers the 21 signal configuration tools on the Flywheel MCP server (`https://api.flywheel.cx/mcp`). For the server itself and how to connect, see [/mcp/overview](/mcp/overview).

## How the pieces fit together

The signal tools configure one pipeline. Read it left to right:

1. **An event arrives** for a person (from your app, an integration, or Flywheel itself).
2. **Event rules check it.** Every enabled rule whose `on_event`, `filters` and `occurrence` match writes its own signal, unless the `event_rules` producer is paused. Configure with `create_signal_rule` / `update_signal_rule`.
3. **The reviewer may be booked.** If the reviewer is on (`update_reviewer_settings` with `reviewer_enabled: true`) and the org has review allowance left, a matching review trigger (`create_review_trigger`) books a review of that person, after the trigger's delay. Independently, an event classified `critical` in the event dictionary (`update_event_dictionary`) books a review with no trigger at all. A daily staleness backstop also books active people who have not been reviewed for `reviewer_staleness_days`.
4. **The reviewer runs** on the booked person: it reads their dossier, your `org_brief`, and your enabled actions with their `guidance` (`list_signal_actions`), and answers with up to `reviewer_max_findings` findings. Each finding must cite evidence from the dossier or it is dropped.
5. **Admitted findings are delivered** according to the chosen action's `delivery`: `signal` writes a signal, `watch` sets a tripwire for a later review, `none` is recorded on the review log only.
6. **The signal is stamped with its routing** from its rule (or action): `handler` human or agent, `rung` draft, approve or auto, and an optional `default_assignee_user_id`. It lands in the signals list in its `bucket`.
7. **The signal closes**: worked by a person, resolved as won by a purchase (`purchase_resolves`), or expired after `expiry_minutes`.

<Note>
  Some routing fields are saved and validated but not yet acted on: an agent is not started for `handler: "agent"` with `rung: "auto"`, `follow_up` does not start an agent run, `resolve_on` events do not close signals, and the `$fw_signal_created` event is not emitted. You can configure them now; they have no effect on people until Flywheel starts acting on them.
</Note>

<Note>
  Event rules and the reviewer are two separate producers. A signal from either looks the same downstream: same list, same routing fields. Pausing one does not affect the other: event rules are paused with `set_signal_producer_status`, the reviewer is switched off with `update_reviewer_settings`.
</Note>

## Permissions for signal tools

Every signal tool declares one permission, checked against the MCP connection's credential:

| Permission | Tools |
| - | - |
| `signals.read` | `list_signal_rules`, `get_signal_rule`, `list_review_triggers`, `list_signal_actions`, `list_event_dictionary`, `get_reviewer_settings`, `list_signal_agents`, `list_signal_producers` |
| `signals.write` | `create_signal_rule`, `update_signal_rule`, `delete_signal_rule`, `create_review_trigger`, `update_review_trigger`, `delete_review_trigger`, `create_signal_action`, `update_signal_action`, `delete_signal_action`, `update_event_dictionary`, `update_reviewer_settings`, `set_signal_producer_status` |
| `agents.write` | `set_signal_agent_cap` |

`delete_signal_rule`, `delete_review_trigger` and `delete_signal_action` are marked destructive. Any id you pass (`handler_agent_id`, `follow_up.agent_id`, `default_assignee_user_id`, `scan_segment_id`) must be a UUID that belongs to your organization; ids from another org are refused.

## Routing: handler, rung and assignee

Signal rules (`create_signal_rule`) and reviewer actions (`create_signal_action`, `update_signal_action`) share the same routing fields:

* `handler`: `human` (default) or `agent`. Who works the signal.
* `handler_agent_id`: the agent that works it. Required on a rule when `handler` is `agent`, and refused when `handler` is `human`. Get ids from `list_signal_agents`.
* `rung`: the autonomy level, `draft` (default), `approve` or `auto`. Stamped on every signal the rule or action writes. On reviewer actions it can never exceed the action's `rung_ceiling`.
* `default_assignee_user_id`: the team member the signal is assigned to. Must be a member of the org.
* `follow_up`: `{ "after_minutes": 4320, "agent_id": "..." }`, meant to hand a still-unresolved signal to an agent after a delay. `after_minutes` is 5-86400 (60 days), `agent_id` must be one of your org's agents, and on a rule `after_minutes` must be shorter than `expiry_minutes`.

On an update, switching `handler` to `human` without passing `handler_agent_id` drops the agent automatically.

Agent dispatch and follow-ups are saved and validated but not yet acted on: a signal with `handler: "agent"` and `rung: "auto"` does not start its agent, and a `follow_up` does not start a run. They are designed to let agents message people without per-message approval once they take effect, so confirm with the person you are working for before setting them.

## Signal rules (event rules)

A signal rule turns a matching event into a signal. Tools: `list_signal_rules`, `get_signal_rule`, `create_signal_rule`, `update_signal_rule`, `delete_signal_rule`. Rule writes take effect at the next event after a cache of up to one minute turns over.

Every matching rule writes its own signal: rules are independent detectors, not a first-match pipeline. Two rules on the same event produce two signals, each deduplicated on its own.

### list\_signal\_rules and get\_signal\_rule

`list_signal_rules` takes no arguments and returns `{ "rules": [...] }` in display order. `get_signal_rule` returns `{ "rule": {...} }` for one id, or not found if the rule is not in your org.

```json theme={null}
{ "rule_id": "6a1d2f0e-4b7c-4f3a-9c55-2e8d7b1a0c44" }
```

Each rule carries every field below plus `id`, `position`, `sent_count`, `undone_count` and `created_at`.

### create\_signal\_rule

Creates a rule. Only `on_event` and `bucket` are required; everything else has a default. Returns `{ "rule": {...}, "action": "created" | "updated" }`.

If `key` matches an existing rule, that rule is **replaced whole**: fields you leave out are reset to their defaults, not kept. That makes creation with a `key` safe to retry. Without a `key`, every call creates a new rule. To change one field of an existing rule, use `update_signal_rule`.

```json theme={null}
{
  "key": "E1_ai_credits_limit",
  "label": "Hit AI credit limit",
  "bucket": "expansion",
  "on_event": "hit_limit",
  "filters": [{ "property_path": "limit", "op": "eq", "value": "ai_credits" }],
  "occurrence": { "count": 3, "window_minutes": 20160 },
  "handler": "human",
  "default_assignee_user_id": "0b9e4c1a-7d2f-4e8b-a3c6-5f1d9e2b7a80",
  "expiry_minutes": 10080,
  "dedupe": { "type": "calendar", "unit": "week" },
  "resolve_on": [
    { "event": "entered_stage", "filters": [{ "property_path": "stage", "op": "eq", "value": "active" }] }
  ],
  "contact_reason": "Hit their AI credit limit three times in two weeks",
  "fact_properties": ["limit", "plan.tier"]
}
```

`on_event` cannot be `$fw_signal_created`; a rule on it is refused.

### update\_signal\_rule

Changes some fields of a rule. Pass `rule_id` plus only the fields you want to change; omitted fields keep their stored value. Passing `null` clears the nullable fields (`follow_up`, `handler_agent_id`, `occurrence`, and also `key`, `label`, `default_assignee_user_id`, `contact_reason`). Returns `{ "rule": {...} }`.

```json theme={null}
{
  "rule_id": "6a1d2f0e-4b7c-4f3a-9c55-2e8d7b1a0c44",
  "enabled": false
}
```

The merged rule is validated as a whole, so an edit that leaves the rule inconsistent (for example `handler: "agent"` with no agent, or a `follow_up` no longer shorter than `expiry_minutes`) is refused. Setting a `key` another rule already has is refused.

### delete\_signal\_rule

Deletes a rule. Signals it already wrote keep their history (bucket, handler and rung live on each signal). Returns `{ "deleted": true }`. To stop a rule without losing it, use `update_signal_rule` with `"enabled": false` instead.

```json theme={null}
{ "rule_id": "6a1d2f0e-4b7c-4f3a-9c55-2e8d7b1a0c44" }
```

### Signal rule fields

| Field | Type | Default | Notes |
| - | - | - | - |
| `key` | string or null | null | Stable name, 1-64 letters, digits, `_`, `.` or `-`, starting with a letter or digit. Unique in the org. |
| `label` | string or null | the bucket | Human name, up to 120 characters. |
| `bucket` | string | required | The queue bucket, e.g. `expansion` or `payment_failing`. Up to 80 characters. |
| `enabled` | boolean | true | Disabled rules are skipped. |
| `on_event` | string | required | Event name exactly as it arrives, e.g. `hit_limit`, `entered_stage`, `$fw_stripe_invoice_payment_failed`. |
| `filters` | array | `[]` | Up to 8, AND-ed. Each is `{ property_path, op, value? }` with `op` one of `eq`, `neq`, `contains`, `exists`, `absent`. Filters read the **event's** properties only, not person properties. `property_path` is a dotted path such as `plan.tier`. |
| `occurrence` | object or null | null | `{ count, window_minutes }`: fire on the Nth occurrence within the window. `count` 2-100, `window_minutes` 5-129600. Null fires on every occurrence. |
| `handler`, `handler_agent_id`, `rung`, `default_assignee_user_id`, `follow_up` | | | See "Routing: handler, rung and assignee". |
| `queue` | boolean | true | `false` means the rule matches but writes nothing. |
| `expiry_minutes` | integer | 4320 (72h) | How long the signal is worth working, 15-129600. Counted from the event's time. |
| `dedupe` | object | one per person per calendar day | See "Signal rule dedupe". |
| `purchase_resolves` | boolean | false | A succeeded purchase closes the rule's open signals as won. |
| `resolve_on` | array | `[]` | Saved and validated, not yet acted on. See "Closing signals with resolve\_on". |
| `contact_reason` | string or null | a sentence naming the event and rule | The "why contact them" line on the signal, up to 200 characters. |
| `fact_properties` | string\[] | `[]` | Up to 12 event properties copied onto the signal as facts. |

### Signal rule dedupe

`dedupe` on `create_signal_rule` / `update_signal_rule` decides what counts as the same signal. A repeat detection inside the window lands on the existing signal instead of opening a new one. Dedupe is always scoped to the rule.

| `dedupe` | Meaning | Use for |
| - | - | - |
| `{ "type": "calendar", "unit": "day" }` | One signal per person per UTC hour, day, week or month (`unit`). The default is `day`. | Behaviour rules where repeats inside the period are one piece of work. |
| `{ "type": "property", "path": "invoice_id" }` | One signal per distinct value of an event property. If the event lacks the property, it falls back to one signal per event. | Payment failures: dunning retries of one charge stay on one signal, a new charge opens a new one. |
| `{ "type": "event" }` | One signal per event occurrence. | Events that are each their own piece of work. |
| `{ "type": "lifetime" }` | Once per person, ever. | One-time milestones. |

### Closing signals with resolve\_on

`resolve_on` on a signal rule lists outcome events meant to close the rule's open signals for the same person as **won** when they arrive after the signal opened. It is saved and validated but not yet acted on: today an arriving `resolve_on` event does not close anything, and signals close by being worked, by a purchase (`purchase_resolves`), or by expiring. Up to 8 entries, each `{ "event": "...", "filters": [...] }` with up to 8 equality filters (`op` must be `eq`). A number and its decimal string compare equal (`"80"` matches `80`). An entry with no filters matches any event of that name. `$fw_signal_created` cannot resolve a signal.

```json theme={null}
{
  "rule_id": "6a1d2f0e-4b7c-4f3a-9c55-2e8d7b1a0c44",
  "resolve_on": [
    { "event": "entered_stage", "filters": [{ "property_path": "stage", "op": "eq", "value": "active" }] }
  ],
  "purchase_resolves": true
}
```

Use `purchase_resolves: true` only on rules whose point is causing a purchase. A churn-risk rule with it on would be marked won when the person buys something unrelated.

## Review triggers

A review trigger summons the AI reviewer to look at a person when an event happens. Triggers do not write signals themselves; they book a review, and the review's findings may become signals. Tools: `list_review_triggers`, `create_review_trigger`, `update_review_trigger`, `delete_review_trigger`.

Triggers only book reviews while the reviewer is on (`reviewer_enabled`) and the org has review allowance left (see `get_reviewer_settings`). Each trigger fire costs a review unless it is merged or skipped (see "Cost and volume").

### list\_review\_triggers

No arguments. Returns `{ "triggers": [...] }` in creation order, each with `id`, `label`, `on_event`, `property_filters`, `occurrence`, `delay_minutes`, `focus`, `priority`, `enabled`, `created_at`, `updated_at`.

### create\_review\_trigger

Returns `{ "trigger": {...} }`. Only `on_event` is required.

| Field | Default | Notes |
| - | - | - |
| `label` | none | Up to 120 characters. |
| `on_event` | required | The event that summons the reviewer. Cannot be `$fw_signal_created`. |
| `property_filters` | `[]` | Same grammar as rule `filters`: up to 8, AND-ed, over event properties. |
| `occurrence` | null | `{ count, window_minutes }`, same limits as on rules. |
| `delay_minutes` | 0 | 0-43200 (30 days). Review this long after the event. A delay lets absences form: `720` reviews their first 12 hours. |
| `focus` | null | A question for the reviewer to answer about this person, up to 500 characters. Shown to the reviewer before the dossier. |
| `priority` | `normal` | `critical` lets the review skip the debounce window. |
| `enabled` | true | |

```json theme={null}
{
  "label": "First 12 hours after signup",
  "on_event": "signed_up",
  "delay_minutes": 720,
  "focus": "Did they reach their first successful API call? If not, what stopped them?",
  "priority": "normal"
}
```

### update\_review\_trigger

Pass `trigger_id` plus the fields to change; omitted fields keep their value. `occurrence: null` and `focus: null` clear those fields. Returns `{ "trigger": {...} }`.

```json theme={null}
{ "trigger_id": "c2f7a9e1-3d4b-4a6c-8e1f-9b0d2c5e7a13", "enabled": false }
```

### delete\_review\_trigger

Deletes a trigger. Returns `{ "deleted": true }`.

```json theme={null}
{ "trigger_id": "c2f7a9e1-3d4b-4a6c-8e1f-9b0d2c5e7a13" }
```

## Signal actions (the reviewer's action catalog)

The action catalog is the closed set of verbs the AI reviewer may answer with, and how each verb's signals are routed. Tools: `list_signal_actions`, `create_signal_action`, `update_signal_action`, `delete_signal_action`.

Every org starts with five shipped actions:

| `key` | `delivery` | `handler` | `rung` | `rung_ceiling` | Meaning |
| - | - | - | - | - | - |
| `human_reachout` | `signal` | human | draft | draft | A person should contact them directly. Can never send itself. |
| `agent_draft` | `signal` | agent | draft | auto | An agent writes the message; lands as a draft for approval. Ships with no agent chosen, so its signals fall back to a human handler until you set `handler_agent_id`. |
| `alert` | `signal` | human | auto | auto | Flag to the team now, ahead of everything else. |
| `watch` | `watch` | human | auto | auto | No outward action; set a tripwire and look again later. |
| `none` | `none` | human | auto | auto | Nothing warranted. The normal answer for most people. |

`delivery` decides what an admitted finding produces: `signal` writes a signal (the only outward path), `watch` sets a tripwire for a later review, `none` is recorded on the review log only.

`guidance` goes into the reviewer's prompt **verbatim**, and the reviewer is told to follow it over its own reading of the verb's name. Only enabled actions are offered to the reviewer; a finding naming a disabled or unknown action is dropped.

### list\_signal\_actions

No arguments. Returns `{ "actions": [...] }` sorted by key, each with `id`, `key`, `label`, `guidance`, `delivery`, `rung`, `rung_ceiling`, `enabled`, `handler`, `handler_agent_id`, `default_assignee_user_id`, `follow_up`, `sent_count`, `undone_count`, `created_at`, `updated_at`.

### create\_signal\_action

Adds a verb. Required: `key` (lowercase letters, digits and `_`, starting with a letter, up to 64 characters, unique in the org), `label`, `delivery`. Optional: `guidance`, `rung` (default `draft`), `rung_ceiling` (default `auto`), `enabled` (default true), `handler` (default `human`), `handler_agent_id`, `default_assignee_user_id`, `follow_up`. Returns `{ "action": {...} }`.

`rung_ceiling` is fixed at creation and can never be raised. `rung` may not exceed it, and a signal is always born at the lower of the two. Choose the ceiling deliberately: a ceiling of `draft` guarantees the action never sends on its own.

```json theme={null}
{
  "key": "renewal_check_in",
  "label": "Renewal check-in",
  "guidance": "Use when an annual account is within 30 days of renewal and usage has dropped. Not for monthly plans.",
  "delivery": "signal",
  "handler": "human",
  "rung": "draft",
  "rung_ceiling": "approve",
  "default_assignee_user_id": "0b9e4c1a-7d2f-4e8b-a3c6-5f1d9e2b7a80"
}
```

### update\_signal\_action

Changes routing or guidance. Pass `action_id` plus fields to change: `label`, `guidance`, `delivery`, `rung`, `enabled`, `handler`, `handler_agent_id`, `default_assignee_user_id`, `follow_up` (`null` clears it). `rung_ceiling` cannot be changed. Switching `handler` to `human` without passing `handler_agent_id` drops the agent. Returns `{ "action": {...} }`.

Route `agent_draft` to an agent at the `auto` rung (stored now; agents are not yet started automatically):

```json theme={null}
{
  "action_id": "9d3e1b7c-2a4f-4c8e-b6d0-1f5a7c9e3b21",
  "handler": "agent",
  "handler_agent_id": "4e8a2c6f-1b3d-4f7a-9c2e-6d0b8f1a3c57",
  "rung": "auto"
}
```

### delete\_signal\_action

Deletes a verb. Permanent: the shipped catalog is only seeded when an org's catalog is empty, so a deleted shipped verb does not come back. Findings that name it are dropped. To stop the reviewer using a verb reversibly, use `update_signal_action` with `"enabled": false`. Returns `{ "deleted": true }`.

```json theme={null}
{ "action_id": "9d3e1b7c-2a4f-4c8e-b6d0-1f5a7c9e3b21" }
```

## Event dictionary

The event dictionary says what each event means to the AI reviewer. Its `salience` is the most consequential reviewer setting you have: it decides what the reviewer's dossier keeps. Tools: `list_event_dictionary`, `update_event_dictionary`.

| Field | Values | Effect |
| - | - | - |
| `salience` | `critical`, `notable`, `routine`, `noise`, or null | `noise` never reaches the dossier. `critical` and `notable` appear as day-by-day timeline lines (critical first; notable lines are capped by recency). `routine` appears as rollups. Null (unclassified) reads as `routine`. A person's first-ever occurrence of an event surfaces regardless of salience. |
| `critical` (extra effect) | | When the reviewer is on, every occurrence of a `critical` event books a review of that person, with no review trigger needed. |
| `expected_next` | `{ event_name, within_hours }` or null | "This event usually leads to that one within N hours." When the follow-on event has not happened once the window has passed, the dossier shows an absence fact, which is often the strongest evidence a reviewer has. `within_hours` 1-2160. |
| `category` | `error`, `conversion`, `engagement`, `lifecycle`, `billing`, `system` | Classification. |
| `description` | up to 500 characters | What the event means, in words. |
| `salience_source` | `ai`, `stats`, `operator`, `shipped` (read-only) | Who classified it. Trust order is operator, then stats, then AI. `shipped` marks Flywheel's own `$fw_*` events. |
| `admin_verified` | boolean (read-only) | Set by an operator edit. |
| `counts_as_activity` | boolean (read-only) | Whether the event counts toward a person's daily activity. False for machine-written state events such as `entered_stage` and for noise. |

Events are classified automatically (by AI, and by a stats pass that demotes an event one person fires more than 500 times in 7 days). Operator edits outrank both.

### list\_event\_dictionary

Returns `{ "events": [...], "total": N }` sorted by event name, including Flywheel's shipped `$fw_*` classifications. `query` is a case-insensitive substring of the event name; `limit` defaults to 200, maximum 1000. `total` counts all matches, so a `total` above your `limit` means there are more.

```json theme={null}
{ "query": "invoice", "limit": 50 }
```

### update\_event\_dictionary

Classifies one event as an operator. Pass `event_name` and at least one of `description`, `salience`, `category`, `expected_next` (`null` clears it). The edit sets `admin_verified` and `salience_source: "operator"`, so later AI and stats passes will not overwrite it. Returns `{ "event": {...} }`.

```json theme={null}
{
  "event_name": "api_key_created",
  "salience": "notable",
  "category": "engagement",
  "description": "The person created an API key. Usually followed by a first successful API call.",
  "expected_next": { "event_name": "api_request_succeeded", "within_hours": 48 }
}
```

You can only classify events your org has already sent. Classifying an unseen event returns not found ("send one event first, then classify it"), because a dictionary row is also the event's validation schema.

<Warning>
  Setting an event to `critical` makes every occurrence of it book a review while the reviewer is on. A high-volume event marked `critical` spends review allowance fast. Use `notable` for events that matter but happen often.
</Warning>

## Reviewer settings

The AI reviewer's org-level settings, plus where the org stands against its review allowance. Tools: `get_reviewer_settings`, `update_reviewer_settings`.

### get\_reviewer\_settings

No arguments. Returns `{ "reviewer": {...} }`:

| Field | Default | Meaning |
| - | - | - |
| `reviewer_enabled` | false | Whether the reviewer runs at all. |
| `scan_segment_id` | null | The segment the reviewer's scan sweep is meant to be scoped to. Stored, but not yet applied by the scan sweep, which still looks at everyone tracked. |
| `org_brief` | null | Read by the reviewer on every review, verbatim: what the company sells, who the customer is, what activation means. |
| `reviewer_debounce_minutes` | 240 | After a review, the person is left alone this long. A review booked inside the window is canceled before the model runs, unless its priority is `critical`. |
| `reviewer_staleness_days` | 21 | Someone active but not reviewed for this many days is booked by the daily backstop. |
| `reviewer_max_findings` | 3 | Cap on findings per review. Findings past the cap are dropped by position. |
| `allowance` | | `{ allowed, reason, used, limit, period_start, period_end }`. `reason` is `not_on_plan`, `limit_reached` or `guardrail_reached` when `allowed` is false. `limit: null` means unlimited; `used: null` means the plan has no count limit. |

### update\_reviewer\_settings

Pass only the fields to change; omitted fields keep their value. Returns `{ "reviewer": {...} }` with the fresh allowance.

| Field | Range |
| - | - |
| `reviewer_enabled` | boolean |
| `scan_segment_id` | a segment UUID in your org, or `null` for everyone |
| `org_brief` | up to 8000 characters, or `null` to clear |
| `reviewer_debounce_minutes` | 15-2880 |
| `reviewer_staleness_days` | 3-120 |
| `reviewer_max_findings` | 1-5 |

```json theme={null}
{
  "org_brief": "We sell a usage-billed API for transcription. Activation is the first successful transcription request. Customers on the Team plan are worth a human conversation; self-serve users are not.",
  "reviewer_max_findings": 2
}
```

Switching the reviewer on (from off) requires the AI Reviewer add-on on the plan, and is refused with a failed-precondition error if it is missing. An org that has merely used up its allowance for the period keeps its switch; reviews pause and resume when the period renews or the plan changes. Read `allowance` before enabling: every review is a paid model call that counts against it.

Settings changes reach the event path within about a minute.

## Signal agents and their caps

Agents are built in the Flywheel dashboard's agent builder. The MCP tools only read them and set how many signals each may be handed. Tools: `list_signal_agents`, `set_signal_agent_cap`.

### list\_signal\_agents

No arguments. Returns `{ "agents": [...] }`, each with `id`, `name`, `status`, `version`, `signal_daily_dispatch_cap` and `signal_dispatches_today` (signal runs started for this agent since 00:00 UTC). Call this before routing anything to an agent: the `id` is what `handler_agent_id` and `follow_up.agent_id` take.

### set\_signal\_agent\_cap

Sets how many signals one agent may be handed per UTC day, counting first dispatches and follow-ups together. Range 0-1000; the default for a new agent is 25. `0` means the agent is never handed signals. Requires `agents.write`. Returns `{ "agent": {...} }`. Agents are not yet started on signals automatically, so the cap is stored ahead of that.

```json theme={null}
{ "agent_id": "4e8a2c6f-1b3d-4f7a-9c2e-6d0b8f1a3c57", "signal_daily_dispatch_cap": 10 }
```

The cap is designed as the rail against runaway sends. Set it before routing any rule or action to `handler: "agent"` with `rung: "auto"`, so it is already in place when agent dispatch takes effect.

## Signal producers

Producers are the deterministic signal detectors: `event_rules` (all your signal rules) and `webinar_session_ended` (the post-webinar queue). Tools: `list_signal_producers`, `set_signal_producer_status`. The AI reviewer is not a producer here; its switch is `reviewer_enabled` in `update_reviewer_settings`.

### list\_signal\_producers

No arguments. Returns `{ "producers": [{ "key", "configured", "status" }] }`. `status` is `active`, `paused` or `archived`. `configured: false` means the detector has never run or been toggled for this org; it reads as `active`, which is what it will be when it first fires.

### set\_signal\_producer\_status

Pauses or resumes one detector. A paused detector writes no signals: no event rule fires, and the webinar build skips. `status` is `active` or `paused`. Returns the full producer list.

```json theme={null}
{ "key": "event_rules", "status": "paused" }
```

Pausing `event_rules` is the fastest way to stop every rule at once without editing or deleting any of them.

## The \$fw\_signal\_created event

`$fw_signal_created` is a Flywheel event name reserved for announcing new signals, so that workflows (playbooks) can react to them. It is not emitted yet: no signal currently produces it, so a workflow triggered on it will not run. See [/mcp/workflows](/mcp/workflows) for workflows in general.

The name is already protected: `$fw_signal_created` is refused as a signal rule's `on_event`, as a `resolve_on` event, and as a review trigger's `on_event`.

## Cost and volume

What spends money (review allowance) and what creates more signals.

**Changes that increase reviews (cost):**

* `update_reviewer_settings` with `reviewer_enabled: true`. Nothing is reviewed while it is off.
* Each `create_review_trigger`. Every matching event books a review. Several bookings for one person merge into one pending review, and a review inside `reviewer_debounce_minutes` of the last one is canceled before the model runs.
* `priority: "critical"` on a trigger, which skips the debounce.
* `update_event_dictionary` with `salience: "critical"`, which books a review on every occurrence of that event.
* Lowering `reviewer_debounce_minutes` or `reviewer_staleness_days`.

**Changes that increase signal volume:**

* A new or enabled signal rule, especially on a frequent event with no `filters` or `occurrence`.
* A narrower `dedupe` (`event` instead of `calendar`), or a longer `expiry_minutes` (more signals open at once).
* A higher `reviewer_max_findings`, or a new action with `delivery: "signal"`.
* Resuming a paused producer.

**Ways to cut volume without deleting anything:** `enabled: false` on a rule, trigger or action; `queue: false` on a rule; `set_signal_producer_status` to pause `event_rules`; \`\`reviewer\_enabled: false\`.

## Side effects on real people

Signal configuration changes what lands in your team's signals list and what the AI reviewer looks at. None of the signal tools sends a message to a person by itself today.

Two kinds of setting are designed to message people once Flywheel acts on them, and are saved and validated now:

* **`handler: "agent"` with `rung: "auto"`** on a rule or action (`create_signal_rule`, `update_signal_rule`, `create_signal_action`, `update_signal_action`): meant to start the agent on every new signal, up to its daily cap, with nobody approving each run.
* **A `follow_up`** on a rule or action: meant to start an agent on every signal still unresolved after `after_minutes`.

Confirm with the person you are working for before setting either, and set the agent's cap with `set_signal_agent_cap` first, so the configuration is already safe when it takes effect.

What does have an effect today: rules, actions and producers decide which people your team is asked to contact (and `default_assignee_user_id` decides who), and reviewer settings, triggers and dictionary salience decide how many paid reviews run.

## Common tasks

### Flag accounts that keep hitting a limit

<Steps>
  <Step title="Check the event and its properties">
    Call `list_event_dictionary` with `{ "query": "hit_limit" }` to confirm the event exists and how it is named. Rule `filters` read event properties, so make sure the property you filter on (here `limit`) is sent on the event.
  </Step>

  <Step title="Create the rule with a key">
    Call `create_signal_rule` with `key`, `bucket`, `on_event`, `filters`, an `occurrence` and a `dedupe`, routed to a human. The `key` makes the call safe to retry.
  </Step>

  <Step title="Pick the expiry and outcome">
    Set `expiry_minutes` to how long the signal is worth working. If the rule's point is a purchase, set `purchase_resolves: true` so a purchase closes it as won.
  </Step>
</Steps>

### Route a rule's signals to an agent

Agents are not yet started on signals automatically, so this records the routing ahead of time.

<Steps>
  <Step title="Pick the agent and set its cap">
    Call `list_signal_agents`, choose the agent, and call `set_signal_agent_cap` with a conservative cap such as `{ "agent_id": "...", "signal_daily_dispatch_cap": 10 }`.
  </Step>

  <Step title="Route the rule">
    Call `update_signal_rule` with `{ "rule_id": "...", "handler": "agent", "handler_agent_id": "..." }`. Leave `rung` at `draft` unless the person you are working for has confirmed they want the agent to act without approval once dispatch takes effect; then add `"rung": "auto"`.
  </Step>
</Steps>

### Turn on the AI reviewer

<Steps>
  <Step title="Check the allowance">
    Call `get_reviewer_settings`. If `allowance.reason` is `not_on_plan`, the AI Reviewer add-on is needed first.
  </Step>

  <Step title="Write the org brief">
    Call `update_reviewer_settings` with an `org_brief`: what you sell, who your customers are, what activation and expansion look like.
  </Step>

  <Step title="Tune the catalog">
    Call `list_signal_actions` and adjust `guidance` with `update_signal_action` so each verb says when your team wants it used. Route `agent_draft` to an agent if you have one.
  </Step>

  <Step title="Classify the events that matter">
    Use `update_event_dictionary` to mark key events `notable`, demote chatter to `noise`, and add `expected_next` for the steps of your funnel.
  </Step>

  <Step title="Add triggers and switch it on">
    Create one or two review triggers with `create_review_trigger`, then call `update_reviewer_settings` with `{ "reviewer_enabled": true }`.
  </Step>
</Steps>

### Ask the reviewer to check new signups after a day

Call `create_review_trigger`:

```json theme={null}
{
  "label": "Day-one review",
  "on_event": "signed_up",
  "delay_minutes": 1440,
  "focus": "Has this person done the one thing that predicts they stay? If not, is a human worth their time?"
}
```

### Teach the reviewer your funnel

Call `update_event_dictionary` for each step, with `expected_next` pointing at the step after it. An unmet step becomes an absence fact in the dossier once the window passes.

```json theme={null}
{ "event_name": "workspace_created", "expected_next": { "event_name": "teammate_invited", "within_hours": 72 } }
```

### Stop everything quickly

* All event rules: `set_signal_producer_status` with `{ "key": "event_rules", "status": "paused" }`.
* The reviewer: `update_reviewer_settings` with `{ "reviewer_enabled": false }`.

## Mistakes to avoid

* **Calling `create_signal_rule` with an existing `key` to change one field.** It replaces the rule whole and resets every field you left out to its default. Use `update_signal_rule`.
* **Calling `create_signal_rule` without a `key` on retry.** Each call creates another rule. Give rules a `key`.
* **Filtering a rule on a person property.** `filters` and `property_filters` read the event's properties only.
* **Routing to an agent without setting its cap first.** The default cap is 25 per UTC day. Set it with `set_signal_agent_cap` before `rung: "auto"` goes on.
* **A `follow_up` longer than `expiry_minutes`.** Refused by validation.
* **Marking a frequent event `critical` in the dictionary.** Every occurrence books a review. Use `notable`.
* **Classifying events before they have been sent.** `update_event_dictionary` returns not found for an event your org has never sent.
* **Deleting a shipped action to stop the reviewer using it.** Deletion is permanent and it is never re-seeded. Set `enabled: false` instead.
* **Choosing `rung_ceiling` loosely on a new action.** It can never be raised later, and it is the guarantee that an action never sends on its own.
* **Using `purchase_resolves` on a rule unrelated to buying.** Any purchase marks its signals won.
* **Expecting a rule edit to apply instantly.** Rules are cached for up to a minute; the next event after that sees the change.
* **Building a rule or a workflow on `$fw_signal_created`.** A rule on it is refused, and the event is not emitted yet, so a workflow on it never runs.
* **Relying on `resolve_on`, `follow_up` or agent dispatch.** They are saved and validated but not yet acted on.
* **Inventing ids.** Agent, team member, segment, rule, trigger and action ids come from the list tools here, from [/mcp/people](/mcp/people) for people and segments, and must belong to your org.

Related: [/mcp/overview](/mcp/overview), [/mcp/workflows](/mcp/workflows), [/mcp/people](/mcp/people), [/mcp/broadcasts](/mcp/broadcasts), [/mcp/webinars](/mcp/webinars).


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