# Trigger stdlib

> The trigger stdlib exposes the live runtime registry to Harn scripts. Use it to inspect installed bindings, register new bindings at runtime, fire synthetic events for...

Website: https://harnlang.com/stdlib/triggers.html

This page documents Harn, which is pre-1.0. Language, standard library, and CLI APIs may change. If the intended version is unclear, clarify before using this page.

---

The trigger stdlib exposes the live runtime registry to Harn scripts. Use it to
inspect installed bindings, register new bindings at runtime, fire synthetic
events for tests/manual invocations, replay a recorded event by id, and inspect
the current dead-letter queue (DLQ).

Import the shared types from `std/triggers` when you want typed handles and
payloads:

```harn
import "std/triggers"
```

## Builtins

### `harness.runtime.trigger_list()`

Return the current live registry snapshot as `list<TriggerBinding>`.

Each binding includes:

- `id`
- `version`
- `source` (`"manifest"` or `"dynamic"`)
- `kind`
- `provider`
- `autonomy_tier`
- `handler_kind`
- `state`
- `metrics`

`metrics` is a typed `TriggerMetrics` record with counters for `received`,
`dispatched`, `failed`, `dlq`, `in_flight`, and the cost snapshot fields.

### `harness.runtime.trigger_register(config)`

Register a trigger dynamically and return its `TriggerHandle`.

`TriggerConfig` uses the same broad shape as manifest-loaded bindings:

- `id`
- `kind`
- `provider`
- `autonomy_tier`
- `handler`
- `when`
- `when_budget`
- `retry`
- `match` or `events`
- `dedupe_key`
- `filter`
- `allow_cleartext`
- `budget`
- `manifest_path`
- `package_name`

Dynamic `harness.runtime.trigger_register(...)` currently supports the legacy
budget fields but does not yet accept manifest-only flow-control tables such
as `concurrency`, `throttle`, `rate_limit`, `debounce`, `singleton`, `batch`,
or keyed `priority`.

The runtime currently accepts two handler forms:

- Local Harn closures / function references
- Remote URI strings with `a2a://...` or `worker://...`

Local handler and predicate entrypoints receive
`(harness: Harness, event: TriggerEvent)`. Predicates may therefore perform
governed classification, including model calls, before dispatch. Inside either
boundary, ordinary helpers should accept the smallest coherent capability
interface they need rather than inheriting the root Harness.

`allow_cleartext` is optional and only applies to `a2a://...` handlers. Set it
to `true` when you intentionally want HTTP A2A discovery / dispatch, for
example when talking to a local `harn serve` process during development.

`retry` is optional. The current stdlib surface accepts:

- `{max: N, backoff: "svix"}`
- `{max: N, backoff: "immediate"}`

Example:

```harn
import "std/triggers"

fn handle_issue(harness: Harness, event: TriggerEvent) -> dict {
  return {kind: event.kind, provider: event.provider}
}

fn main(harness: Harness) {
  const handle: TriggerHandle = harness.runtime.trigger_register({
    id: "github-new-issue",
    kind: "issue.opened",
    provider: "github",
    autonomy_tier: "act_with_approval",
    handler: handle_issue,
    allow_cleartext: nil,
    when: nil,
    when_budget: nil,
    match: {events: ["issue.opened"]},
    events: nil,
    dedupe_key: nil,
    filter: nil,
    budget: nil,
    manifest_path: nil,
    package_name: nil,
  })
}
```

### `harness.runtime.trigger_fire(handle, event)`

Fire a synthetic `TriggerEvent` into a binding and return a
`DispatchHandle`.

The builtin accepts either:

- A `TriggerHandle` / `TriggerBinding` dict
- A plain trigger id string

If the `event` dict omits low-level envelope fields such as `id`,
`received_at`, `trace_id`, or `provider_payload`, the runtime fills them with
synthetic defaults.

Current behavior:

- Execution routes through the trigger dispatcher, so local handlers inherit
  dispatcher retries, lifecycle events, action-graph updates, and DLQ moves.
- `when` predicates execute before the handler and can still short-circuit a
  dispatch.
- `when_budget` accepts `{max_cost_usd, tokens_max, timeout}` and applies
  fail-closed per-predicate LLM cost governance.
- When a manifest-installed binding uses `batch = { ... }`, the selected leader
  event carries the coalesced member list in `event.batch`.
- `a2a://...` handlers return either the inline remote result or a pending task
  handle, depending on the peer response.
- `worker://...` handlers return an enqueue receipt in `DispatchHandle.result`
  with `{queue, job_event_id, response_topic}`.

### `harness.runtime.trigger_replay(event_id)`

Replay a previously recorded event from the EventLog by id and return a
`DispatchHandle`.

Current replay behavior:

- Fetch the prior event from the `triggers.events` topic
- Re-dispatch it through the trigger dispatcher using the recorded binding
- Preserve `replay_of_event_id` on the returned `DispatchHandle`
- Resolve the pending stdlib DLQ entry when a replay succeeds

`harness.runtime.trigger_replay(...)` is still not the full deterministic T-14
replay engine. It replays the recorded trigger event through the current
dispatcher/runtime state rather than a sandboxed drift-detecting environment.

### `harness.runtime.trigger_inspect_dlq()`

Return the current DLQ snapshot as `list<DlqEntry>`.

Each `DlqEntry` includes:

- The failed `event`
- Trigger identity (`binding_id`, `binding_version`)
- Current `state`
- Latest `error`
- Derived `error_class` (`provider_5xx`, `predicate_panic`,
  `handler_panic`, `handler_timeout`, `auth_failed`, `budget_exhausted`, or
  `unknown`)
- `retry_history`

`retry_history` records every DLQ attempt, including replay attempts.

### `harness.runtime.trigger_inspect_lifecycle(kind?)`

Return the trigger lifecycle stream as a list of `{kind, headers, payload}`
records. Pass a kind such as `predicate.evaluated`,
`predicate.budget_exceeded`, or `DispatchStarted` to filter on the runtime
side.

### `harness.runtime.trigger_inspect_action_graph(trace_id?)`

Return streamed `observability.action_graph` records as a list of
`{kind, headers, payload}` records. Pass a `trace_id` to filter the stream to
one trigger delivery and its downstream predicate, dispatch, retry, worker,
A2A, and DLQ nodes.

### `harness.runtime.handler_context()`

Return the current dispatch context as `HandlerContext | nil`.

Inside a trigger handler, the returned record includes:

- `agent`
- `action`
- `trace_id`
- `replay_of_event_id`
- `autonomy_tier`
- `trigger_event`

Outside trigger dispatch, the method returns `nil`.

## Stream helpers

`std/triggers` includes small declarative helpers for stream handlers:

- `stream_fork(source, branches)` returns a `StreamForkPlan`
- `stream_join(source, join?)` returns a `StreamJoinPlan`
- `window_by(events, window)` returns a `StreamWindowPlan`
- `llm_classify(input, labels, options?)` returns a `StreamLlmClassifyPlan`

These helpers do not start broker consumers by themselves. They give handlers a
typed, serializable plan shape for fan-out/fan-in, manifest-aligned windowing,
and cached classifier steps.

```harn
import "std/triggers"

pub fn on_quotes(harness: Harness, event: TriggerEvent) -> dict {
  const forked = stream_fork(
    [event.provider_payload.raw], ["risk", "ledger"],
  )
  const windowed = window_by(
    [event.provider_payload.raw],
    {
      mode: "sliding",
      key: "event.provider_payload.key",
      size: "5m",
      every: "1m",
      gap: nil,
      max_items: 500,
    },
  )
  const classified = llm_classify(
    event.provider_payload.raw, [
      "ignore", "review",
    ], {cache: "quotes:v1"},
  )
  return {forked: forked, windowed: windowed, classified: classified}
}
```

### `harness.runtime.trust_record(agent, action, approver, outcome, tier)`

Append a manual OpenTrustGraph `TrustRecord` to the trust graph. Scripts usually
rely on the dispatcher's automatic end-of-handler records, but this builtin is
available for control-plane events such as promotions, demotions, or manual
audit entries. The returned record includes `chain_index`, `previous_hash`, and
`entry_hash`, and the runtime also mirrors a compact projection to
`trust_graph.records`.

### `harness.runtime.trust_graph_record(decision)`

Append a decision dict to the trust graph and return its `TrustEntryId`
(`record_id`). The dict accepts `agent`/`actor_id`, `action`, `approver`,
`outcome`, `trace_id`, `autonomy_tier`/`autonomy_tier_at_time` (or `tier`),
`evidence_refs`, `cost_usd`, and `metadata`.

### `harness.runtime.trust_graph_query(agent, action)`

Return a `TrustScore` for an agent and optional action. Scores include outcome
counts, success rate, latest outcome, effective autonomy tier, and a capability
policy that handlers can use as a gate.

### `harness.runtime.trust_graph_policy_for(agent)`

Return only the derived `CapabilityPolicy` for an agent.

### `harness.runtime.trust_graph_verify_chain()`

Verify the active trust graph hash chain and return a report with `verified`,
`root_hash`, `broken_at_event_id`, and `errors`.

### `harness.runtime.trust_query(filters)`

Query historical trust records from Harn code.

Supported filter keys:

- `agent`
- `action`
- `since`
- `until`
- `tier`
- `outcome`
- `limit`
- `grouped_by_trace`

`limit` keeps only the newest N matching records. When `grouped_by_trace` is
`true`, the builtin returns `list<{trace_id, records}>` trace buckets instead of
the default flat `list<TrustRecord>`.

For new code, prefer `import "std/trust"` and call `query(...)` or
`trust.query(...)` to receive the compact issue-facing `TrustGraphRecord` shape
with `actor_id`, `evidence_refs`, and `autonomy_tier_at_time`.

## Example

```harn
import "std/triggers"

fn fail_handler(harness: Harness, event: TriggerEvent) -> any {
  throw("manual failure: " + event.kind)
}

fn main(harness: Harness) {
  const handle = harness.runtime.trigger_register({
    id: "manual-dlq",
    kind: "issue.opened",
    provider: "github",
    handler: fail_handler,
    when: nil,
    when_budget: nil,
    retry: {max: 1, backoff: "immediate"},
    match: nil,
    events: ["issue.opened"],
    dedupe_key: nil,
    filter: nil,
    budget: nil,
    manifest_path: nil,
    package_name: nil,
  })

  const fired = harness.runtime.trigger_fire(
    handle, {provider: "github", kind: "issue.opened"},
  )
  const dlq = harness.runtime.trigger_inspect_dlq().filter({ entry ->
    entry.binding_id == handle.id
  })
  const replay = harness.runtime.trigger_replay(fired.event_id)

  harness.stdio.log(fired.status)                // "dlq"
  harness.stdio.log(len(dlq[0].retry_history))   // 1
  harness.stdio.log(replay.replay_of_event_id)   // original event id
}
```

## Notes

- Dynamic registrations are runtime-local.
  `harness.runtime.trigger_register(...)` updates the live registry in the
  current process; it does not rewrite `harn.toml`.
- `a2a://...` bindings default to HTTPS-only. Use `allow_cleartext: true` only
  for intentional local or otherwise trusted HTTP peers.
- `TriggerConfig.autonomy_tier` defaults to `act_auto` when omitted.
- `harness.runtime.trigger_fire(...)` and
  `harness.runtime.trigger_replay(...)` need an active EventLog to
  persist `triggers.events` and `triggers.dlq`. If the runtime did not already
  install one, the stdlib wrapper falls back to an in-memory log for the
  current thread.
- Predicate replay is deterministic for `harness.llm.call(...)`: cached predicate
  responses are reused from the request cache plus the per-event
  `trigger.inbox` record rather than calling the live provider again.
- Every terminal dispatch appends one `TrustRecord` to `trust.graph` plus the
  per-agent topic `trust.graph.<agent_id>`.
- When `workflow_execute(...)` runs inside a replayed trigger dispatch, the
  runtime carries the replay pointer into run metadata so derived
  observability can render a `replay_chain` edge back to the original event.

---

## Read next

- [Triggers](https://harnlang.com/triggers.md)
- [Trigger manifests](https://harnlang.com/triggers/manifest.md)
