A trigger declares when a node fires without a human pressing Run. Three kinds cover the useful shapes: a wall-clock cron schedule, a reactive change trigger that fires when upstream data moves, and a threshold trigger that fires a writeback only when a predicate over the node’s own output holds. Triggers live on a node’s config.triggers and are authored declaratively alongside the rest of the graph. The load-bearing idea is identity: an unattended run that touches an external system authorizes as a service principal you bind to it — never as you, and never as an anonymous system actor. The authoring surface enforces this, so an unsafe automation is rejected when you write it, not at 06:00 when it fires.

The shape

Every trigger shares a small envelope and carries exactly one per-kind block:
  • kindcron, change, or threshold. Exactly the matching per-kind block must be present.
  • enabled — omit or true to arm the trigger; only an explicit false disables it (a disabled trigger is inert but preserved, handy for rollback).
  • runs_as — the service principal the fired run authorizes as. Required for any unattended side effect (see Firing identity).
  • acknowledged — your explicit sign-off that an auto-firing writeback is intended (see Acknowledgement).
  • id — server-minted and content-addressed; leave it unset when authoring. It stays stable across re-saves and only changes when the trigger’s definition changes.

Cron — fire on a schedule

  • expression — a standard 5-field cron (minute hour day-of-month month day-of-week).
  • timezone — an IANA name (e.g. Europe/Dublin); omit for UTC.
  • catchup (on the envelope, cron-only) — none (default) or one: run a single catch-up pass for fire windows the scheduler missed while it was down.

Change — fire when upstream data moves

  • upstream_node_ids — the upstream nodes to watch; an empty list watches all direct upstreams.
  • debounce_ms — coalesce rapid upstream changes into a single fire within this window.
  • predicate — optional. A serialized boolean expression (same shape as a threshold predicate) that gates whether a change actually fires; omit to fire on any change.
A change trigger placed directly on a writeback node is the natural way to say “deliver whenever the input refreshes.” The writeback is never part of the compute plan, so it does not deliver as a side effect of the upstream run — a reactor bridges the completed upstream run to the writeback invocation, firing it as the trigger’s runs_as principal.

Threshold — fire a writeback when a predicate holds

A threshold trigger sits on the node whose output it watches and fires a named writeback only when a predicate over that output is true:
  • fires_writeback_node_id — the writeback this threshold invokes. Required, and must resolve to a writeback node.
  • predicate — required. A boolean expression over the trigger.threshold scope, which exposes the watched output’s summary stats: artifact.row_count, and per column artifact.<col>__null_count, artifact.<col>__value_count, artifact.<col>__min, artifact.<col>__max. Example source strings: artifact.row_count = 0, artifact.amount__null_count > 10. It’s authored as an ExpressionSpec; the compiler pins it to a canonical AST hash, so author it through the tooling rather than hand-writing the hash.
  • cooldown_ms — suppress re-firing within this window after a fire.
  • input_mappings — optional map of writeback input name → a template expression resolved from the firing context.
Dry-run a threshold before you commit it with the core_threshold_dry_run MCP tool: it reports whether the predicate evaluated, whether it matched the node’s latest output, and whether the firing identity resolves — without firing anything.

Firing identity

A credential plus an unattended trigger equals unattended external access, and must name a durable principal.
A scheduled or reactive run authorizes as system:scheduler, not as you. Your own grants never apply to it. That is deliberate: the run outlives your authoring session, you may leave, and your grants can change without anyone touching the pipeline. So any unattended run that reaches an external system must name a service principal — a durable, grantable identity — via the trigger’s runs_as (or the node’s config.runs_as as a fallback). Binding one is a three-step path an agent or the API can drive end to end:
  1. Create a service principal in the tenant.
  2. Grant it what the run needs — for a writeback, Secret:READ on the referenced secret and Pipeline:RUN on the graph.
  3. Set runs_as on the trigger (act-as-gated at bind time, so you can only bind a principal you’re allowed to act as).
A run that dispatches with no resolvable identity is refused loudly (TRIGGER_IDENTITY_UNRESOLVED) rather than silently downgraded to a system actor.

Acknowledgement

Identity says who fires; acknowledgement says you meant it to. An auto-firing (cron or change) trigger on a writeback node must set acknowledged: true — an explicit confirmation that you intend an unattended external side effect. Without it the write is rejected at authoring time.

What the authoring surface enforces

Three rules reject an unsafe automation the moment you write it, not when it fires: The last rule is why a scheduled Python node that calls to_sql with a stored secret can’t ship without an identity: a credential plus a schedule is unattended external access. The rejection names the remedy directly — “a scheduled run authorizes as system:scheduler, not as you — your own grants do not apply.”