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.