The problem
An agent that only runs when someone clicks is a tool. An agent that runs on a schedule, or when a pull request opens, is infrastructure — and infrastructure that can act on its own needs answers to questions a manual run never raises. What stops a broken trigger from running forever? A cron expression that fires every minute against an agent whose model is misconfigured will spend money and fill the task table until someone notices. What happens to the schedule when the process restarts? Scheduling inside the API process means a deploy silently drops every timer. Where does the answer go? A task started from a Telegram message that finishes by writing to the dashboard has failed at its actual job. And how does the platform decide whether an event deserves a run at all? Most webhook payloads are noise. Filtering them in the agent means paying for a model call to conclude “nothing to do”.How AgentArea approaches it
A trigger is a stored record, not a running thing.Trigger carries the
agent it starts, the task_parameters used to build the task, a conditions
block, and its own safety counters. Two concrete types subclass it:
Cron triggers are Temporal schedules, not in-process timers. Creating one
calls
create_schedule with id cron-trigger-<trigger_id> and a
ScheduleSpec(cron_expressions=[…], time_zone_name=…). The schedule lives in
Temporal, so it survives a restart of every AgentArea process, and it is visible
and pauseable in the Temporal UI like any other schedule.
Webhook triggers get an unguessable id. When webhook_id is omitted at
creation the platform generates one with secrets.token_urlsafe(16). The URL is
the credential for a generic webhook, which is why it is not derived from the
trigger name.
Webhook types are open, not enumerated. WebhookType names the ones the
platform knows — telegram, slack, github, discord, linear, stripe,
gmail, teams, generic — but its _missing_ accepts any string, so a
deployment can configure a type the enum never heard of instead of being
rejected.
A trigger disables itself before it does damage. Every trigger has a
failure_threshold (default 5, bounded 1–100) and a consecutive_failures
counter. A success resets it; crossing the threshold takes the trigger out of
service. This is the answer to “what stops a broken trigger” — nothing external
has to notice.
Conditions are evaluated before a task is created, and by default they are
evaluated by a model. A condition’s type defaults to llm, which sends the
payload and the condition text to a model instance through litellm and acts on
the verdict. The point is filtering noise without writing a parser per provider;
the cost is a model call per candidate event, which the Limits
section revisits.
Every attempt is recorded, whether or not it produced a task.
TriggerExecution stores status (success, failed, timeout,
cancelled), the task_id if one was created, execution_time_ms, the error,
and the Temporal workflow_id and run_id. A trigger that silently stopped
producing tasks is diagnosable from its own history.
Channels carry the output back
A channel is an outbound adapter with two jobs: format a workflow event for its destination, and send it there. Adapters register into a process-local registry by name:
The split in that last column is deliberate. Only a provider that pushes
updates to a URL you registered needs a registrar; a channel that polls or holds
a gateway connection has none, and the orchestrating service treats the absence
as a no-op rather than an error.
Delivery is a stream job, not an inline call. The workflow emits to an
outbound stream; a consumer reads it through a Redis Streams consumer group,
deduplicates, calls the adapter, then acknowledges, requeues, or dead-letters.
A separate autoclaimer re-delivers messages left pending by a consumer that
died. A slow Telegram API therefore cannot block the agent loop, and a crashed
sender loses nothing.
Why not cron inside the API process
It is one dependency fewer and a few lines of code. It fails the moment you run more than one API replica — every replica fires the same schedule — and it fails again on every deploy, because in-process timers die with the process that holds them and nothing records that they were due. Temporal is already a required dependency for running tasks at all, and it already solves exactly this: durable schedules with a visible history. Adding a second, weaker scheduler next to it would be a choice to maintain two.Why conditions default to a model rather than a rule language
A rule language is cheaper, deterministic, and testable, and for a payload shape you control it is the better tool. The case triggers actually face is the opposite: a webhook body whose shape is set by a third party and changes without notice, where the interesting question is semantic — “is this issue actually a bug report” — rather than structural. A rule language answers that by growing into a small programming language, and every provider needs its own. The trade accepted here is a model call per candidate event, made explicit rather than hidden, with the deterministic path still available by setting an explicit conditiontype.
Limits
Verified against the code on 2026-09-13. Where this section and the model above disagree, this section describes what a deployment actually gets. Condition evaluation costs a model call per candidate event. With the defaultllm condition type, a high-volume webhook pays for one inference per
delivery before any task exists, so the spend does not appear against the
agent’s task budget. Size the source, not the agent.
The adapter registry is per-process. Adapters register on import and on a
configuration call, into an in-memory dict. A channel is available to a process
that registered it; there is no shared registry to query and no error if a
process is asked for a channel it never registered — get_adapter returns
None.
Only Telegram registers its own inbound webhook. Every other channel that
needs a provider-side webhook has to be pointed at the platform by hand, and
nothing in the platform reports that this step was skipped.
A trigger that disables itself stays disabled. Crossing failure_threshold
stops the trigger; nothing re-enables it when the underlying problem is fixed.
Re-enable it explicitly after fixing the cause, or the schedule stays silent.
Signature verification is opt-in, for every type — including github and
stripe. verify_webhook_signature first resolves a signing secret from the
trigger’s validation_rules and webhook_config. With no secret it returns
None, meaning “not enabled”, and the request proceeds. A github webhook
created without a secret is no more verified than a generic one; the
unguessable webhook_id is then the only thing protecting it.
When a secret is configured, the verifier is chosen by type — slack,
github, discord (Ed25519), linear and stripe have dedicated verifiers,
generic uses a configurable HMAC whose header, algorithm and prefix come from
validation_rules, and Telegram is validated by bot token at a different layer.
A configured secret that fails to verify rejects the request, as does a
configured secret with no raw body to check.
Related
Tasks
What a trigger creates, and the states it can reach.
Events
What a channel formats and delivers.
Durable execution
The Temporal machinery cron triggers reuse.
Budgets and quotas
Why unattended runs need a ceiling.