Skip to content
AgentEnv Framework

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

EndpointBodyWhat it does
POST /triggers/registertriggers; optional watch_roles, executorAdds each trigger by id and arms it
POST /triggers/removeidsDisarms and deletes those triggers, ignoring unknown ids
POST /triggers/clearnoneDeletes every trigger, the settings and the firing log
GET /triggers/statenoneEach trigger's status and counts, and the firing log
Terminal
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"}}]}'
Output
{"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.

when
{"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:

when
{"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.

FieldFires
atAt an RFC 3339 time, or a duration after the clock's start such as PT15M
after, offsetoffset after the trigger named in after has fired
everyAgain 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, untilWith 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.

typeFieldsWhat it does
permissionaction (enable or disable), role, tools (a list or "*")Sets the same rules as /tools/enable and /tools/disable
tooltool, argsCalls the tool directly on the server, so role rules don't apply
nlinstructionHands 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.

actions
[{"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

StatusMeans
armedWaiting for its condition
firingIts actions are running
queuedA repeating trigger matched again while firing; pending counts the calls waiting
firedDone: a one-off trigger that succeeded, or a time trigger past its last time
failedA 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"].

Two steps
[
  {"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

Ask a question · Report an issue

On this page