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
- An agent calls the
await_signaltool with a description of what it's waiting for. - A signal task is created and the signal matcher starts watching every inbound that flows through the platform.
- The agent's current chat keeps running. The signal sits in the background.
- When an inbound message looks like a candidate, the matcher invokes the signal-owner agent in a dedicated signal chat to judge it.
- 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.
| Mode | What happens on a match | Use for |
|---|---|---|
| Once (default) | First match completes the task and resumes the parent chat | 2FA codes, single replies, webhook callbacks |
| Continuous | Each match runs the agent in the signal chat which records the hit and keeps watching | Monitoring 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:
| Metadata | Source | Used as |
|---|---|---|
| Categories | An annotator agent classifies the message via annotate_message. Examples: verification_code, auth, reply, urgent. | Primary match signal: overlap with the watch's expected_categories |
| Channel | The channel kind that delivered the message (e.g., sms, telegram) | Hard filter: only candidates from expected_channels are evaluated |
| Contact | Who the message is from, if the platform knows them | Hard 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:
{ "type": "string", "pattern": "^[0-9]{6}$" }Use for verification codes, OTPs, structured tokens.
{ "type": "string", "enum": ["yes", "no", "cancelled"] }Use for confirmations and structured replies.
{
"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
| Tool | Used by | Purpose |
|---|---|---|
await_signal | The waiting agent | Register a watch with criteria, expiry, and an optional result schema |
annotate_message | Channel inference | Categorize an inbound message so the matcher can score it |
report_signal | The signal-owner agent (continuous mode) | Record a match without ending the watch |
complete_task / fail_task | The signal-owner agent | Terminate 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:
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_schemafor 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
- Channels. Where signal candidates come from.
- Scheduling Recurring Work. Other ways to run agents asynchronously.
- Policies.
receive_signaland source-level allow/deny rules.