Skip to content

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

ts
import type { 
DAGLifecycleStateType
} from '@studnicky/dagonizer/lifecycle';

Discriminated union of the six states a DAG lifecycle can occupy:

ts
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:

ts
// 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().

ts
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:

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

ts
import { 
DAGLifecycleMachine
} from '@studnicky/dagonizer/lifecycle';
import type {
DAGLifecycleStateType
,
DAGLifecycleEventType
} from '@studnicky/dagonizer/lifecycle';

Class: DAGLifecycleMachine

Pure reducer for DAGLifecycleState. Static class; never instantiated.

ts
import { 
DAGLifecycleMachine
} from '@studnicky/dagonizer/lifecycle';

DAGLifecycleMachine.initial()

ts
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)

ts
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:

FromEventTo
pendingstartrunning
runningsucceedcompleted
runningfail(error)failed
runningcancel(reason?)cancelled
runningtimeouttimed_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.

ts
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:

ts
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.

Watched over by the Order of Dagon.