Table of Contents

πŸ‡«πŸ‡· Version franΓ§aise

Scripting DSL reference (.ork.ts)

Every builder, every method, and the one column that exists nowhere else: which of the two script shapes actually honours it.

The signatures live in src/scripting/Orkeon.Scripting/Typings/*.d.ts, concatenated at build into the orkeon.d.ts your editor reads. This page does not repeat them. It answers the question the declarations cannot: does the engine that will run my file read this at all?

For the narrative version β€” what to write, in what order, and why β€” see Write a crew in TypeScript.

The two shapes

A .ork.ts file is dispatched to one of two different engines, chosen by how the file ends. RunCommand.DeclaresCrewHandoffAsync reads the source looking for an assignment to globalThis.crew, and routes on what it finds.

Procedural Declarative
the file ends with await crew.run() globalThis.crew = crew
engine ScriptHost.RunFromFileAsync β†’ JsCrew.RunAsync shared runner β†’ JsCrewConfigurationAdapter β†’ ICrewOrchestrationService
runs agent .body() yes, one per agent, in declaration order no β€” ignored
honours withTask / process / manager no β€” ignored yes
orkeon run --validate fails (did not assign globalThis.crew) works

They are opposites, not variants. JsCrew.RunAsync iterates _agents and never looks at tasks; Process reaches only a telemetry tag. The adapter, symmetrically, contains zero occurrences of body and of Budget.

The rule: write .body() and you are procedural; write withTask and you are declarative. Never both in one file. A crew with tasks that ends with await crew.run() runs its agents and ignores every task you wrote β€” logging a warning that says exactly that, which is the only reason the mistake is now cheap to find.

What each shape reads

Measured against JsCrewConfigurationAdapter.cs and JsCrew.cs, not inferred.

agentBuilder()

Method Procedural Declarative
name role goal backstory βœ… βœ… (role falls back to name)
llm βœ… βœ…
tools([...]) built-ins by name βœ… βœ…
withAutonomousTool(s) instances βœ… βœ…
maxIterations verbose allowDelegation βœ… βœ…
body βœ… the whole point ❌ never invoked
withState β†’ ctx.state βœ… ❌
onError βœ… ❌
onAgentStart / onAgentStop βœ… ❌
concurrency only 1 β€” see below ❌
onCommand neither: it is the CLI dispatch seam, not a run-time hook

crewBuilder()

Method Procedural Declarative
name goal verbose βœ… βœ…
withAgent(s) βœ… βœ…
withTask(s) ❌ ignored βœ… the whole point
process ❌ telemetry tag only βœ… ("graph" selects the domain's retry-and-route strategy, not a script-drawn topology β€” see below)
manager ❌ βœ…
memory ❌ βœ…
budget βœ… ❌ ignored
onCrewStart / onCrewComplete / onCrewError βœ… ❌

Everything marked ❌ on the declarative side is now announced: the run logs one warning per dropped declaration, naming the method and the agent it was written on. It is still dropped β€” the two shapes are two engines β€” but a crew no longer runs a body that was never invoked and says nothing about it.

process("graph") is not stateGraph

They are different facilities with a shared word. process("graph") runs the crew on the domain's graph strategy: a fixed agent_execute β†’ route_decision loop with a circuit breaker, tuned by GraphConfig. stateGraph({ nodes, edges }) is the topology you draw, and it runs when you call .run() on it β€” from an agent .body(), so procedurally.

crewBuilder().graph(g) used to blur the two: it accepted a stateGraph, stored it in a field no engine ever read, and process("graph") refused to build without it. The mode was gated behind a method that discarded its argument. The method is gone; process("graph") now builds on its own.

taskBuilder()

Declarative only β€” the procedural engine never reads tasks. name, description, agent, expectedOutput, withContext(s) (this is what builds the DAG), expect, tools, withTaskTool, humanInput, asyncExecution, deliverable.

toolBuilder()

Both shapes. name, description, withSchema, execute, access, build.

The schema is not read by the type system: toolBuilder() with no type argument gives execute an unknown input, and every field access on it is an error. State the shape the schema promises:

const wordCount = toolBuilder<{ text: string }, { words: number }>()
    .name("word_count")
    .withSchema({ type: "object", properties: { text: { type: "string" } }, required: ["text"] })
    .execute((input) => ({ words: input.text.trim().split(/\s+/).length }))
    .build();

The globals

Three names the runner exchanges with the script, declared in globals.d.ts.

Global Direction Meaning
crew script β†’ runner The declarative handoff. Assigning it is what selects the declarative engine.
inputs runner β†’ script What --inputs / --inputs-file parsed. Procedural shape only.
result script β†’ runner What the run reports. Defaults to the last expression's value. Serialised to JSON, so assign a plain projection β€” a CrewResult holds host objects and does not survive the trip.

ctx β€” the agent context

Procedural shape only; nothing in the declarative path constructs one.

ctx.state carries the one legal mutator, with(). It replaces the state with what the callback returns, under the agent's state mutex β€” so carry the fields you are not changing:

await ctx.state.with(prev => ({ ...prev, count: prev.count + 1 }));

Assigning directly (ctx.state.count = 1) throws StateMutationOutsideWithException. The state is a JS Proxy whose set trap exists to make that loud rather than lost.

ctx.signal is a .NET CancellationToken projected through interop, not a DOM AbortSignal β€” there is no DOM in Jint. It carries IsCancellationRequested and CanBeCanceled, CLR-cased; aborted, addEventListener and throwIfAborted are all undefined. Pass it along (crew.run({ signal: ctx.signal })) rather than polling it.

ctx.llm β€” generate, chat, stream, embed, act. act is the LLM ⇄ tool-call loop: it runs the agent's own tool catalogue until the model stops asking or maxIterations is reached. ActOptions.system seeds a real role:"system" message that persists across every iteration; without it act() sends a single user message.

ctx.memory, ctx.events, ctx.lock, ctx.spawn, ctx.delegate, ctx.send/receive/ broadcast, ctx.log. See context.d.ts.

stateMachine and stateGraph

Both are literal-driven and both were declared wrongly until 2026-09-07; if you are working from an older orkeon.d.ts, this is the section to read.

stateMachine takes { name, initial, states }. Transitions live inside each state, keyed by event name, and the destination field is target:

const fsm = stateMachine({
    name: "order", initial: "pending",
    states: {
        pending:  { transitions: { approve: { target: "approved" } }, onEntry: (c) => {} },
        approved: { transitions: { ship: { target: "shipped" } } },
        shipped:  {},
    },
});
await fsm.send("approve");   // resolves to the NEW state name

send() returns the state it ended in β€” which is the current state, unchanged, when the event is unknown to it or a guard refused. An unknown event is not an error. There is no run() method and no circuitBreaker option.

stateGraph takes { name, nodes, edges, graphConfig? }. edges is an object keyed by source node, not a list of pairs, and START/END are plain strings ("__START__"):

const g = stateGraph<{ done: boolean }>({
    name: "research",
    nodes: { gather: (s) => ({ ...s, done: true }) },
    edges: { [START]: "gather", gather: END },
    graphConfig: { circuitBreakerPreset: "Strict" },
});

TState cannot be inferred from the node bodies β€” a node both takes and returns the state, so inference is circular β€” and falls back to Record<string, unknown>. Annotate the call (stateGraph<OrderState>({...})) to get a node that returns the wrong shape reported.

Known gaps between the typings and the runtime

The declarations and the C# runtime are written in two languages and nothing tied them together until scripts/check-scripting-typings.sh did. What follows is what remains after that gate went green.

Gap Behaviour
budget() in the declarative shape Ignored, and said so: the run logs a warning naming the method. Use process("autonomous") in the procedural shape.
.body() in the declarative shape Ignored, and said so: the run logs a warning naming the agent. The most expensive confusion in the DSL, and the reason for the shape table above.
globalThis.inputs in the declarative shape Never planted. Passing --inputs, --inputs-file or --memory-limit-mb to a declarative script now prints a warning on stderr instead of dropping the flag in silence.
A plain { provider, model } object passed to llm() Dropped. ExtractLlmConfig returns null for anything that is not a JsLlmConfig, so build one with llm.openai({...}), llm.default(), etc.
ctx.llm.embed Returns a stub vector. Real embedders are a follow-up.
concurrency(n) with n > 1 Rejected at build with a clear message β€” V1 is a mutex, the N-holder semaphore is V1.5. Loud, not silent.
Locks have no timeout LockTimeoutError is V1.5.
Events In-memory only; no Redis/NATS persistence.
FSM/Graph composition Sub-states and sub-graphs are V1.5.

Conditional inclusion

There is no builder method for it. when(predicate) used to be declared on all three builders and was honoured by neither engine β€” JsCrewBuilder.when and JsTaskBuilder.when discarded the argument outright, and the predicate JsAgentBuilder stored was never read, so .when(() => false) included the agent anyway. It was removed rather than implemented: a method that silently does the opposite of what it says is worse than no method.

Guard the call instead:

const b = crewBuilder().name("nightly");
if (shouldAudit) b.withAgent(auditor);

Editor setup

The typings ship as orkeon.d.ts next to the CLI build output and inside the Orkeon package at content/typings/orkeon.d.ts. Point your editor at it and copy tools/scripting-typecheck/tsconfig.base.json, which is the configuration the repository's own gate uses.

Three of its options are load-bearing:

  • moduleDetection: "force" β€” without it, top-level await crew.run() is rejected (TS1375) and a top-level const crew collides with the crew global (TS2451). Two errors that have nothing to do with your code.
  • target/lib ES2022 β€” what Jint supports. Asking for more promises APIs that are not there; the DOM in particular is absent, which is why ctx.signal is not an AbortSignal.
  • types: [] β€” there is no Node in a .ork.ts. process, require and Buffer do not exist at run time and should not exist at authoring time either.

Where to go next