Lifecycle
What It Is
The lifecycle model records where a DAG run is: pending, running, completed, failed, cancelled, or timed_out.
Use this page when inspecting state.lifecycle, writing deterministic lifecycle tests, projecting execution results, or attaching observability hooks that care about run state.
How It Works
DAGLifecycleMachine.transition() is the state machine. It consumes explicit lifecycle events with monotonic timestamps and returns a new discriminated-union state. Runtime code stores that state on NodeStateBase.lifecycle.
The lifecycle model is intentionally small: it tracks run status and timing, not business progress. Use progress events or node output for domain-level milestones.
Type: DAGLifecycleStateType
import type { DAGLifecycleStateType } from '@studnicky/dagonizer/lifecycle';Discriminated union of the six states a DAG lifecycle can occupy:
import type { DAGLifecycleStateType } from '@studnicky/dagonizer/lifecycle';
// DAGLifecycleStateType is:
// | { variant: 'pending'; startedAt: null; finishedAt: null; error: null; reason: null }
// | { variant: 'running'; startedAt: number; finishedAt: null; error: null; reason: null }
// | { variant: 'completed'; startedAt: number; finishedAt: number; error: null; reason: null }
// | { variant: 'failed'; startedAt: number; finishedAt: number; error: Error; reason: null }
// | { variant: 'cancelled'; startedAt: number; finishedAt: number; error: null; reason: string }
// | { variant: 'timed_out'; startedAt: number; finishedAt: number; error: null; reason: null }
const _check: DAGLifecycleStateType = {} as DAGLifecycleStateType;All timestamps are monotonic milliseconds from Clock.monotonicMs(). They are relative-time values suitable for duration math, not wall-clock display.
Inspect via state.lifecycle.variant. Narrow to a terminal variant to access its payload:
// lifecycle.variant is a discriminated union:
// 'pending' | 'running' | 'completed' | 'failed' | 'cancelled' | 'timed_out' | 'awaiting-input'.
// Each arm carries only the fields relevant to that outcome (e.g. reason, finishedAt).
const lc = cancelResult.state.lifecycle;
type LifecycleVariant = typeof lc.variant;
const lifecycleLog: Record<LifecycleVariant, () => void> = {
'completed': () => { logger.result(`responded: ${cancelResult.state.draft}`); },
'cancelled': () => {
if (lc.variant === 'cancelled') {
logger.result(`visitor abandoned at: ${lc.reason}`);
}
},
'timed_out': () => { logger.result(`hit deadline at: ${lc.finishedAt}`); },
'failed': () => { logger.result(`execution failed at: ${lc.finishedAt}`); },
'pending': () => { logger.result('lifecycle: pending'); },
'running': () => { logger.result('lifecycle: running'); },
'awaiting-input': () => { logger.result(`parked — awaiting human input (key: ${lc.correlationKey})`); },
};
lifecycleLog[lc.variant]();Type: DAGLifecycleEventType
Events consumed by DAGLifecycleMachine.transition().
import type { DAGLifecycleEventType } from '@studnicky/dagonizer/lifecycle';
// DAGLifecycleEventType is:
// | { type: 'start'; at: number }
// | { type: 'succeed'; at: number }
// | { type: 'fail'; error: Error; at: number }
// | { type: 'cancel'; reason: string; at: number }
// | { type: 'timeout'; at: number }
const _check: DAGLifecycleEventType = {} as DAGLifecycleEventType;The at field carries the monotonic clock value for the transition. Supply Clock.monotonicMs() in production; supply a pinned value in tests for determinism.
Diagrams, Examples, and Outputs
Lifecycle is runtime state rather than graph topology. The links below show where lifecycle state appears during real execution and observability:
- Reference: Execution
- Reference: Dagonizer - observability hooks
What It Lets You Do
The lifecycle reference lets applications distinguish successful completion from failure, cancellation, and timeout without string-parsing logs or inspecting thrown errors.
@studnicky/dagonizer/lifecycle
The lifecycle module exports the discriminated union type, the event union type, and the pure reducer machine.
Code Samples
The code below covers the state union, event union, reducer, terminal checks, and custom-state integration.
Import
import { DAGLifecycleMachine } from '@studnicky/dagonizer/lifecycle';
import type { DAGLifecycleStateType, DAGLifecycleEventType } from '@studnicky/dagonizer/lifecycle';Class: DAGLifecycleMachine
Pure reducer for DAGLifecycleState. Static class; never instantiated.
import { DAGLifecycleMachine } from '@studnicky/dagonizer/lifecycle';DAGLifecycleMachine.initial()
const initial = DAGLifecycleMachine.initial();
// Returns: { variant: 'pending', startedAt: null, finishedAt: null, error: null, reason: null }Seed value for a new state object.
DAGLifecycleMachine.transition(state, event)
declare const state: DAGLifecycleStateType;
declare const event: DAGLifecycleEventType;
const next: DAGLifecycleStateType = DAGLifecycleMachine.transition(state, event);Pure reducer. Returns a new state for legal transitions, returns the input state by reference for illegal transitions (which NodeStateBase.dispatch detects and converts to DAGError).
Terminal states (completed, failed, cancelled, timed_out) return themselves unchanged for all events. Terminal stickiness.
Valid transitions:
| From | Event | To |
|---|---|---|
pending | start | running |
running | succeed | completed |
running | fail(error) | failed |
running | cancel(reason?) | cancelled |
running | timeout | timed_out |
All other transitions return the input unchanged (illegal, detected by NodeStateBase).
DAGLifecycleMachine.isTerminal(state)
true if the state is one of completed, failed, cancelled, or timed_out.
declare const lifecycle: DAGLifecycleStateType;
if (DAGLifecycleMachine.isTerminal(lifecycle)) {
// flow has ended
}Usage in custom state
Callers implementing NodeStateInterface without extending NodeStateBase use DAGLifecycleMachine directly to drive lifecycle transitions:
let lifecycle: DAGLifecycleStateType = DAGLifecycleMachine.initial();
function markRunning(): void {
const next = DAGLifecycleMachine.transition(lifecycle, { type: 'start', at: Clock.monotonicMs() });
if (next === lifecycle) throw new Error('Cannot mark running');
lifecycle = next;
}Details for Nerds
Lifecycle timestamps are monotonic milliseconds, not wall-clock timestamps. They are safe for duration math and deterministic tests, but they are not meant for user-facing date display.
Illegal transitions return the input state by reference. NodeStateBase treats that as an error boundary, which keeps the reducer pure while still making bad runtime transitions visible.
Related Concepts
- Reference: Execution
- Reference: Dagonizer - observability hooks
- Cancellation - cancellation and timeout lifecycle outcomes
- Observability - lifecycle hooks and event projection
- Subclassing State -
NodeStateBase.lifecyclebehavior