Triggers
Make an environment react during a run. When a tool call, a state or a virtual time matches, the gateway calls a tool, changes a role's tools or gives an agent an instruction.
Build a Trigger
Choose when the trigger fires and what it does, then play the agent's run against it. The request,
the run and GET /triggers/state follow the gateway's trigger engine, down to the 400s it answers
for options it refuses.
Built Into the Gateway
The trigger engine comes with the gateway topology and
appears on the instance's card as urn:agentenv:triggers/v1, so email and contacts implement
nothing for it. It checks each call after the server has answered, and a call that returns an error
matches no trigger.
Only calls from the roles in watch_roles count, and the agent's role comes from its AgentEnv-Role
header, default without one. An instance without a gateway gets triggers only if its server
advertises urn:agentenv:triggers/v1 itself.
The Endpoints
| Endpoint | Body | What it does |
|---|---|---|
POST /triggers/register | triggers; optional watch_roles, executor | Adds each trigger by id and arms it |
POST /triggers/remove | ids | Disarms and deletes those triggers, ignoring unknown ids |
POST /triggers/clear | none | Deletes every trigger, the settings and the firing log |
GET /triggers/state | none | Each trigger's status and counts, and the firing log |
curl -sS -X POST http://localhost:61854/triggers/register -H 'content-type: application/json' \
-d '{"watch_roles": ["default"], "triggers": [{"id": "unlock-send", "when": {"type": "action", "tool": "contacts_add", "where": {"args.email": {"regex": "@example\\.com$"}}}, "actions": [{"type": "permission", "action": "enable", "role": "default", "tools": ["send"]}], "barrier": {"at": "provoking_call"}}]}'{"ok": true, "added": ["unlock-send"], "all": ["unlock-send"]}Registering adds to what is there. The same trigger sent again changes nothing, a different one under
a used id gets a 400, and a trigger without an id gets one like trg_3f9a0c1d2e4b.
The first register request fixes watch_roles (["default"] if you leave it out), and the first to
send an executor fixes that; a later request with other values gets a 400 until you clear. Once the gateway knows its tools,
a trigger that names a tool it doesn't serve gets a 400 too. clear is for setup and tests, not the
middle of a run.
When: A Tool Call
An action trigger fires when a watched call to tool returns and every test in where passes. A
where key names an argument (to), a path into the arguments (args.to) or a path into the parsed
result (result.sent), with [0] for a list item, and holds one test: regex, equals or exists.
{"type": "action", "tool": "send", "where": {"args.to": {"regex": "@example\\.com$"}}, "repeat": true}It fires once, or on every match with "repeat": true. A match that arrives while a repeating trigger
is still firing waits its turn, and the trigger shows queued.
When: A State
A state trigger runs its check after each watched call to a tool not marked read-only
(readOnlyHint), and fires the first time the check passes. Each step calls a tool, can extract a
value by path and bind it to a name later steps use as ${name}, and the last step tests it with a
predicate:
{"type": "state", "check": {"steps": [
{"tool": "contacts_lookup", "args": {"name": "Priya"}, "extract": "contacts[0].email", "bind": "address"},
{"tool": "email_search", "args": {"query": "${address}"}, "extract": "emails", "predicate": {"exists": true}}
]}}A one-step check can skip steps, as in the builder. The engine calls the check's tools itself,
directly on the servers, so a check never fires another trigger.
When: A Time
A time trigger fires on the virtual clock, so nothing fires
until the clock is armed. The gateway checks time triggers every real second and on every watched
call.
| Field | Fires |
|---|---|
at | At an RFC 3339 time, or a duration after the clock's start such as PT15M |
after, offset | offset after the trigger named in after has fired |
every | Again and again, every duration, or at random gaps with {"dist": "exp", "mean": "PT1H"} and an integer seed; from at or after if given, otherwise one gap after the start |
count, until | With every: stop after that many firings, or at that time |
If several due times pass between two checks, as they can on a fast clock, the trigger fires once
for each, up to 1,000 at a time. Arming the clock again moves at and every triggers onto the new
timeline and drops what was due on the old one.
Then: Actions
A trigger's actions run in order, and the first one to fail stops the rest.
type | Fields | What it does |
|---|---|---|
permission | action (enable or disable), role, tools (a list or "*") | Sets the same rules as /tools/enable and /tools/disable |
tool | tool, args | Calls the tool directly on the server, so role rules don't apply |
nl | instruction | Hands the instruction to the executor agent and waits for its task to complete |
In an action trigger, a tool action's args can read the provoking call with ${args.<path>} and
${result.<path>}. A placeholder that is the whole value keeps its type, and ${args.cc?} drops the
key when there is nothing to read; a required one with nothing to read fails the action.
[{"type": "tool", "tool": "send", "args": {
"to": "${args.email}", "subject": "Welcome, ${args.name}", "body": "You are in the office contacts."
}}]A tool or permission action can carry a verify check, shaped like a state check, and fails if
the check doesn't pass. An nl action with a verify that fails is sent to the executor once more.
The Executor
nl actions need an executor, sent once in a register body: {"a2a_url": "http://localhost:61907", "role": "world"},
plus timeout_seconds, 120 by default. The gateway sends the instruction with A2A message/send and
polls tasks/get until the task completes or the timeout passes.
The executor's role can't be in watch_roles, so its own tool calls never fire triggers.
Instructions are plain text, never templated, and one that contains ${args. or ${result. gets a
400.
Barriers
With "barrier": {"at": "provoking_call"}, the gateway holds its answer to the call that fired the
trigger until the trigger's actions are done, so the agent's next step already sees their effect.
provoking_call is the only value, and only action triggers can have one.
The gateway holds the call for up to timeout_seconds, 30 by default, and for the longest of them
when several barriers hold one call. Past that it answers anyway and writes trigger_barrier_timeout
to the trajectory.
Statuses and the Firing Log
| Status | Means |
|---|---|
armed | Waiting for its condition |
firing | Its actions are running |
queued | A repeating trigger matched again while firing; pending counts the calls waiting |
fired | Done: a one-off trigger that succeeded, or a time trigger past its last time |
failed | A time trigger whose action failed, now retired |
An action or state trigger that fails goes back to armed, and failure_count and last_failure_at
record the failure. events lists what the engine did, in order: added, removed, detected,
anchored, reanchored, action_ok, action_failed, verify_ok, verify_failed, fired, failed
and eval_error.
The gateway keeps the first 1,000 events and the last 9,000, and events_dropped counts the rest.
Once the clock is armed, every event carries a virtual_time next to its real ts.
In the Trajectory
Every firing is also a trigger_fired event in the env's trajectory,
and every tool action an internal_tool_call with the tool, whether it succeeded and a hash of its
result. Its arguments are recorded by shape, such as {"redacted": {"type": "str", "len": 22}},
except for ids and names like email_id or title.
When a prompt_agent step ends, it reads GET /triggers/state from every env that a
register_env_triggers step registered triggers on. It waits up to a minute for triggers still
firing, then saves the state under env_trigger_state/ in the object store.
From a Task
The register_env_triggers step sends the register request from a task: env_id and triggers,
and optionally watch_roles, executor_agent_name and executor_timeout_seconds (120). It fails on
a 400, and records what it added in metadata["env_trigger_registrations"].
[
{"id": "world", "type": "deploy_agent", "env_ids": ["office"], "agent_name": "world", "role": "world", "depends_on": [{"task_step_id": "office"}]},
{"id": "triggers", "type": "register_env_triggers", "env_id": "office", "watch_roles": ["default"], "executor_agent_name": "world", "triggers": [{"id": "dana-welcomes", "when": {"type": "time", "at": "PT15M"}, "actions": [{"type": "nl", "instruction": "You are Dana Park. Email priya.shah@example.com to welcome her to the Q3 review."}]}], "depends_on": [{"task_step_id": "office"}, {"task_step_id": "world"}]}
]executor_agent_name names an agent that a deploy_agent step deployed with a role outside
watch_roles; the step sends that agent's A2A URL and role as the executor. This time trigger
fires only once a sync_env_clock step arms the
clock, and the other steps that drive the gateway are in Env control.
Agents have triggers of their own, which register_agent_triggers registers on an agent that plays
the user; see Agent extensions.
Last updated on