Trigger observability in the action graph
Harn projects trigger activity into both persisted run observability and the
live dispatcher event stream. The current surface includes trigger,
predicate, dispatch, a2a_hop, worker_enqueue, retry, and dlq
nodes, plus the matching trigger_dispatch, predicate_gate,
a2a_dispatch, retry, dlq_move, and replay_chain edges.
What lands in this change
- A synthetic
triggernode is added when a run carries atrigger_eventenvelope inrun.metadata. - Workflow
conditionstages render aspredicatenodes inobservability.action_graph_nodes. - Local dispatch attempts render as
dispatchnodes. - Remote A2A dispatch attempts render as
a2a_hopnodes labelled with the resolvedtarget_agent. worker://...queue handoffs render asworker_enqueuenodes with queue name, response topic, and job receipt metadata.- Retry scheduling emits
retrynodes with delay/error metadata. - Exhausted deliveries emit
dlqnodes with final-error metadata. - Entry edges from the trigger node into the workflow render as
trigger_dispatch. - Trigger or predicate edges into a remote A2A hop render as
a2a_dispatch. - Transitions leaving a predicate on the workflow path render as
predicate_gate. - Replay-triggered dispatches add a visible
replay_chainedge back to the original trigger event. trace_idpropagates from theTriggerEventonto the synthetic trigger node and every downstream action-graph node derived from that run, including A2A hops and worker queue receipts.- A2A dispatches also send the same trace on the wire via the
A2A-Trace-IdHTTP header in addition to the JSON envelope metadata.
The runtime streams these updates onto the shared event-log topic
observability.action_graph. The portal subscribes to that topic for traced
runs and merges live node/edge updates into the detail view while still
showing the persisted run snapshot.
Portal view
The portal run detail now renders dedicated node cards instead of a plain edge dump:
- trigger cards show provider, event kind, signature status, and trace id
- predicate cards show pass/block state plus cost and latency
- A2A cards render with a dashed trust-boundary style and task metadata
- worker enqueue cards show queue and response-topic details
- DLQ and trigger cards expose a replay button that invokes
harn trigger replay <event-id>through the portal backend
Node metadata
Each RunActionGraphNodeRecord now carries a metadata map so the portal and
CLI consumers can render node-specific details without inventing a second
schema. Representative fields include:
trigger:provider,event_kind,dedupe_key,signature_statuspredicate:trigger_id,predicate,result,cost_usd,tokens,latency_ms,reasondispatch/a2a_hop/worker_enqueue:handler_kind,target_uri,attempt, plus route-specific fields liketarget_agent,task_id,queue_name,response_topic, andjob_event_idretry:delay_ms,errordlq:attempt_count,final_error
Example
When a trigger is dispatched, the streamed action-graph updates now include records like:
{
"kind": "trigger",
"label": "cron:tick",
"trace_id": "trace_123",
"metadata": {
"provider": "cron",
"event_kind": "cron.tick",
"signature_status": "unsigned"
}
}
and:
{
"kind": "worker_enqueue",
"label": "worker://triage",
"trace_id": "trace_123",
"metadata": {
"queue_name": "triage",
"response_topic": "worker.triage.responses",
"job_event_id": 42
}
}
with edges such as:
{"kind": "trigger_dispatch", "from_id": "trigger:...", "to_id": "stage:..."}
{"kind": "predicate_gate", "label": "true"}
{"kind": "a2a_dispatch", "from_id": "predicate:...", "to_id": "a2a:..."}
{"kind": "retry", "from_id": "dispatch:...", "to_id": "retry:..."}
{"kind": "dlq_move", "from_id": "dispatch:...", "to_id": "dlq:..."}