Skip to content

Runner

What It Is

DagRunner is the reusable host loop around a registered DAG. It accepts trigger input, creates initial state, executes a DAG reference resolved by the dispatcher, and projects the final execution result into an application-specific output.

Use this page when a CLI command, queue worker, HTTP route, browser action, or scheduled job should invoke the same registered DAG without rewriting the register→seed→execute→project loop.

How It Works

Subclass DagRunner<TInput, TState, TOutput> and implement two hooks: seedState(input) and projectResult(result). Triggers feed inputs into the runner; the runner delegates execution to a registered Dagonizer.

The runner does not change DAG semantics. It is a hosting convenience for applications that want one stable adapter around many trigger shapes.

Diagrams, Examples, and Outputs

The runner reference is about host integration rather than graph shape. For executable examples, start with Example 28: Runner and Triggers, then compare the triggered DAG registration with the Dagonizer reference.

What It Lets You Do

Use DagRunner to keep trigger plumbing outside DAG node code. The DAG stays portable; the runner adapts external input and output to the host environment.

Code Samples

Abstract class: DagRunner<TInput, TState, TOutput>

The canonical DAG execution harness. Owns the register→seed→execute→route→project loop once. Applications subclass and override seedState and projectResult.

ts
class 
MyRunner
extends
DagRunner
<
MyInput
,
MyState
,
MyOutput
> {
protected override
seedState
(
input
:
MyInput
):
MyState
{
const
state
= new
MyState
();
state
.
value
=
input
.
value
;
return
state
;
} protected override
projectResult
(
result
:
ExecutionResultType
<
MyState
>):
MyOutput
{
return { 'value':
result
.
state
.
value
,
'done':
result
.
state
.
lifecycle
.
variant
=== 'completed',
}; } }

TInput is the trigger-specific input type; TState must satisfy NodeStateInterface (extend NodeStateBase); TOutput is the projected output.

Constructor

ts
const 
dispatcher
= new
Dagonizer
<
MyState
>();
const
options
:
DagRunnerOptionsType
<
MyState
> = { 'dispatcher':
dispatcher
};
const
runner
= new
MyRunner
(
options
);

DagRunnerOptionsType has one required field:

FieldTypeDescription
dispatcherDagonizer<TState>The configured dispatcher the runner drives. Injected via constructor; the runner does not own construction.

registerBundle(bundle)

Delegates to dispatcher.registerBundle. Call before run.

run(dagIri, input, options?)

ts
const 
dispatcher
= new
Dagonizer
<
MyState
>();
const
runner
= new
MyRunner
({
dispatcher
});
const
output
= await
runner
.
run
('urn:noocodec:dag:my-dag', {
value
: 1 });

Builds initial state via seedState(input), calls dispatcher.execute(dagIri, state, options), and returns projectResult(result). Never throws — unexpected errors route through onRunError.

resume(dagIri, state, fromStage, options?)

ts
declare const 
dispatcher
:
Dagonizer
<
MyState
>;
declare const
rehydrated
:
MyState
;
const
runner
= new
MyRunner
({
dispatcher
});
const
output
= await
runner
.
resume
('urn:noocodec:dag:my-dag',
rehydrated
, 'urn:noocodec:dag:my-dag/node/node-b');

Resumes from placement IRI fromStage with a pre-rehydrated state. The caller is responsible for rehydrating state before the call (typically via Checkpoint.load(raw).restoreState(fn)). Never throws.

seedState(input) — abstract

Override to build the initial TState from the trigger input. Runs inside run() before dispatcher.execute.

projectResult(result) — abstract

Override to project ExecutionResultType<TState> to the application's TOutput shape. Runs after each execute/resume call.

onRunError(dagIri, error) — protected

Called when an unexpected error escapes the engine. Default re-throws. Override to absorb the error and return a replacement TOutput when callers need continuity.


Interface: DagRunnerInterface<TInput, TState, TOutput>

ts
declare function 
acceptRunner
(
runner
:
DagRunnerInterface
<unknown, NodeStateInterface, unknown>
): void;

The public face of DagRunner. Trigger implementations accept this interface rather than the concrete class so they are portable across runner subclasses.


Interface: TriggerInterface<TInput, TState, TOutput>

ts
declare const 
trigger
:
TriggerInterface
<unknown, NodeStateInterface, unknown>;

Adapter contract for the timing signal. Ships through @studnicky/dagonizer/runner for ergonomic co-import with the runner classes. Canonical source is @studnicky/dagonizer/contracts.

MethodDescription
attach(runner)Wire the trigger to a runner. Returns a promise that resolves when the trigger's lifecycle ends (all planned invocations done, or detach was called).
detach()Tear down any pending subscription. Idempotent.

Class: OnceTrigger<TInput, TState, TOutput>

ts
declare const 
runner
: import('@studnicky/dagonizer/runner').
DagRunnerInterface
<{
text
: string }, NodeStateInterface, {
words
: number }>;
const
trigger
= new
OnceTrigger
<{
text
: string }, NodeStateInterface, {
words
: number }>(
'urn:noocodec:dag:word-count', {
text
: 'hello world' },
); await
trigger
.
attach
(
runner
);
const
result
=
trigger
.
result
; // available after attach resolves

Fires runner.run(dagIri, input, options) exactly once when attach is called. detach() before attach makes attach a no-op.

MemberDescription
constructor(dagIri, input, options?)Supply the DAG IRI, literal input, and optional ExecuteOptionsType.
resultTOutput | null. Available after attach resolves; null before or if detached before attach.
attach(runner)Fires the run and resolves when done.
detach()Marks the trigger as detached; future attach is a no-op.

Abstract class: CliTrigger<TInput, TState, TOutput>

ts
class 
MyCli
extends
CliTrigger
<{
text
: string }, NodeStateInterface, {
words
: number }> {
protected override
parseArgs
(
_command
: string,
args
: string[]): {
text
: string } {
return {
text
:
args
.
join
(' ') };
} protected override
selectDag
(
_command
: string): string {
return 'urn:noocodec:dag:word-count'; } } const
trigger
= new
MyCli
('word-count',
process
.
argv
.
slice
(2));

Abstract base for CLI harnesses. Subclass and override parseArgs.

MemberDescription
constructor(command, args, options?)command is the primary command token; args are remaining argv tokens.
resultTOutput | null. Available after attach resolves.
parseArgs(command, args) — abstractMap raw argv tokens to TInput. Must override.
selectDag(command) — protectedMap the command token to a registered DAG IRI. Default returns the command token unchanged, so the command token must already be an IRI unless overridden.
attach(runner)Calls parseArgs, then runner.run(selectDag(command), input, options).
detach()Marks trigger as detached; future attach is a no-op.

Abstract class: EventTrigger<TMessage, TInput, TState, TOutput>

ts
type 
Msg
= {
text
: string };
class
MyEvent
extends
EventTrigger
<
Msg
, {
text
: string }, NodeStateInterface, {
words
: number }> {
protected override
subscribe
(
onMessage
: (
msg
:
Msg
) => void): () => void {
// wire to a real event source and return unsubscribe return () => { /* teardown */ }; } protected override
toInput
(
msg
:
Msg
): {
text
: string } {
return {
text
:
msg
.
text
};
} }

Abstract base for subscription-driven harnesses (WebSocket, EventEmitter, message queue). Each inbound message triggers a parallel runner.run call. detach tears down the subscription and resolves the attach promise.

TMessage is the raw message type emitted by the subscription; TInput is the runner input built from it.

MemberDescription
subscribe(onMessage) — abstractRegister a handler with the event source. Return an unsubscribe function.
toInput(message) — abstractConvert a raw message to TInput.
selectDag(message) — protectedChoose the DAG IRI for a message. Default returns urn:noocodec:dag:default.
attach(runner)Registers the subscription and returns a promise that resolves only after detach() is called.
detach()Unsubscribes and resolves the attach promise.

Abstract class: RequestTrigger<TRequest, TInput, TState, TOutput>

ts
type 
Req
= {
body
: string };
class
MyRequest
extends
RequestTrigger
<
Req
, {
text
: string }, NodeStateInterface, {
words
: number }> {
protected override
toInput
(
req
:
Req
): {
text
: string } {
return {
text
:
req
.
body
};
} } const
trigger
= new
MyRequest
();
// await trigger.attach(runner); // const output = await trigger.fire({ body: 'hello world' });

Abstract base for per-turn HTTP harnesses. attach stores the runner reference (no subscription); fire(request) is the entry point from the HTTP handler or turn loop.

MemberDescription
toInput(request) — abstractConvert a raw request to TInput.
selectDag(request) — protectedChoose the DAG IRI per request. Default returns urn:noocodec:dag:default.
requestOptions(request) — protectedSupply per-turn ExecuteOptionsType (signal, deadlineMs). Default returns {}.
attach(runner)Stores the runner reference. Resolves immediately — no subscription.
detach()Clears the runner reference. Subsequent fire calls throw.
fire(request)Calls runner.run(selectDag(request), toInput(request), requestOptions(request)). Throws if called before attach.

Import path

ts
import {
  
DagRunner
,
OnceTrigger
,
CliTrigger
,
EventTrigger
,
RequestTrigger
,
} from '@studnicky/dagonizer/runner'; import type {
DagRunnerInterface
,
DagRunnerOptionsType
,
TriggerInterface
,
} from '@studnicky/dagonizer/runner';

Details for Nerds

@studnicky/dagonizer/runner ships the DagRunner abstract base class and trigger variants. Every callable hangs off a class; there are no freestanding functions.

Triggers are adapters around external input. They should translate host-specific request shape into TInput, choose a DAG reference, and pass execution options. They should not mutate DAG state directly or bypass DagRunner.run().

Watched over by the Order of Dagon.