Skip to content

A signal is a "wait for X to happen" task. The agent registers what it's looking for (a verification code, a reply from a specific contact, a class of message), and the conversation continues. When something matches, the agent resumes automatically with the matched content in hand.

Signals are how you build flows like:

  • "Tell me when the 2FA code arrives, then continue logging in."
  • "Wake me up if anyone in the family-group chat asks for help."
  • "Watch the alerts channel and tag anything urgent for the next hour."

How it works

  1. An agent calls the await_signal tool with a description of what it's waiting for.
  2. A signal task is created and the signal matcher starts watching every inbound that flows through the platform.
  3. The agent's current chat keeps running. The signal sits in the background.
  4. When an inbound message looks like a candidate, the matcher invokes the signal-owner agent in a dedicated signal chat to judge it.
  5. If the candidate is the right one, the agent reports it. The original chat resumes with the result.

The agent never sees the candidate's full content unless it's in the signal chat. The signal chat runs with a restricted tool registry so attacker-controlled message content can't talk the agent into calling unrelated tools.

Two modes

Signals come in two flavors. Pick based on whether you want one match or many.

ModeWhat happens on a matchUse for
Once (default)First match completes the task and resumes the parent chat2FA codes, single replies, webhook callbacks
ContinuousEach match runs the agent in the signal chat which records the hit and keeps watchingMonitoring a channel, tracking mentions, watching for any of an open-ended set of events

A continuous signal stops when its expires_at is reached, when max_evaluations is exhausted, or when the agent explicitly calls complete_task to stop watching.

What you ask agents to do

The agent uses await_signal automatically when you describe a wait-for pattern:

  • "Try logging in to my bank, and when the SMS code arrives, enter it."
  • "Watch my SMS for any verification codes from Stripe and tell me when one shows up."
  • "Keep an eye on the family group chat for the next two hours and ping me if anyone asks for help."

You don't need to mention "signals" or "tools". The agent picks the right shape based on what you said.

How matching works

Every inbound message carries metadata the matcher can use:

MetadataSourceUsed as
CategoriesAn annotator agent classifies the message via annotate_message. Examples: verification_code, auth, reply, urgent.Primary match signal: overlap with the watch's expected_categories
ChannelThe channel kind that delivered the message (e.g., sms, telegram)Hard filter: only candidates from expected_channels are evaluated
ContactWho the message is from, if the platform knows themHard filter: only expected_contacts are evaluated

A watch must specify at least one of categories, channels, or contacts. The matcher uses categories to score candidates and the other two to filter out obviously irrelevant traffic.

When a candidate passes the filters, the signal-owner agent is invoked in the signal chat to make the final call. The agent sees the candidate plus the original watch instructions and decides whether to record it as a match.

Result schemas

Signals can require the result to match a JSON Schema. The result_schema parameter on await_signal constrains both LLM generation (when the model supports it) and validation server-side. This stops attacker-controlled message content from smuggling arbitrary text into the parent chat.

Common shapes:

json
{ "type": "string", "pattern": "^[0-9]{6}$" }

Use for verification codes, OTPs, structured tokens.

json
{ "type": "string", "enum": ["yes", "no", "cancelled"] }

Use for confirmations and structured replies.

json
{
  "type": "object",
  "properties": {
    "is_important": { "type": "string", "enum": ["yes", "no"] },
    "category": { "type": "string" },
    "evidence_quote": { "type": "string", "maxLength": 300 }
  },
  "required": ["is_important", "category"],
  "additionalProperties": false
}

Use for structured judgments in continuous monitoring.

Channels feeding signals

A channel in Signal mode is the typical source of signal candidates. The channel does tag-only inference on every inbound, runs the annotator, and feeds the matcher, but does not reply. This is the right setup for "ingest the verification SMS, don't have the SMS agent reply to the bank".

A channel in Message mode also feeds the matcher: every inbound runs through the annotator before normal inference.

Tools

ToolUsed byPurpose
await_signalThe waiting agentRegister a watch with criteria, expiry, and an optional result schema
annotate_messageChannel inferenceCategorize an inbound message so the matcher can score it
report_signalThe signal-owner agent (continuous mode)Record a match without ending the watch
complete_task / fail_taskThe signal-owner agentTerminate a watch (success / failure)

The signal chat exposes only the terminal/match-recording tools. You can't turn this off; it's a security boundary.

Authorization

Each candidate evaluation goes through the policy engine via the receive_signal action. The default policy allows all receive_signal actions; operators add forbid rules for sensitive sources:

cedar
forbid(
  principal,
  action == Policy::Action::"receive_signal",
  resource in Policy::Channel::"email"
);

See Policies for more.

Tips

  • Be specific about categories. "verification_code" beats "code". The annotator will use the exact label, and so will the matcher.
  • Use channel filters when you know the source. A 2FA flow knows the code will come via SMS. Adding expected_channels: ["sms"] skips evaluation on every random Telegram message.
  • Set realistic expirations. Once-mode watches expire automatically. Make sure the window is wide enough to catch the message you actually care about. Continuous watches need generous expirations because they keep running.
  • Use result_schema for sensitive outputs. A 6-digit pattern protects against injection. An enum protects against the model paraphrasing.
  • Don't overload categories. A small, stable set of category labels works better than dozens of slightly-different tags.

Next steps