Skip to content

Extending mini-ork

mini-ork is designed to be extended without editing executor core modules. Node types, routing policies, gate types, provider transports, subcommands, embedders and whole recipes all have registration APIs. This page shows each surface, the signature it expects, and how to load it safely.

Extension surfaces

Surface Registration call Selected by
Node type register_node_handler(type, fn, phase=) workflow.yaml node type
Implementer fan-out register_implementer_submode(recipe, node, artifact, script) Recipe + node id
Routing policy register_policy(name, fn) MO_ROUTING_POLICY
Gate type register_gate_evaluator(type, fn) Gate row's gate_type
Provider kind register_provider_kind(kind, builder) providers.yaml kind
Dispatch backend register_dispatch_backend(model, fn) Model lane name
Subcommand register_subcommand(name, handler) mini-ork <name>
Embedder register_embedder_provider(name, factory) MO_EMBED_PROVIDER

A recipe may ship a register.py next to its YAML. The loader executes it once per process before the run; this is the supported place to hold recipe-local registrations. See Recipe-local registration.

Node handlers

A node handler runs one node. The executor's preamble — routing, environment publication, gates, prompt assembly — has already happened, so the handler receives a fully prepared NodeDispatch:

from mini_ork.cli.execute_handlers import NodeDispatch, register_node_handler

def handle_invoice_check(nd: NodeDispatch):
    # nd.prompt_ref, nd.lane, nd.run_dir, nd.recipe, nd.task_class, ...
    out = nd.run_dir / "invoice-check.json"
    ok = check_invoices(nd.plan_path, out)
    return (0 if ok else 1), ("succeeded" if ok else "failed")

register_node_handler("invoice_check", handle_invoice_check)

The contract is (NodeDispatch) -> (rc, finish_reason). rc == 0 means the node succeeded; any other value is a failure and the run's recovery logic takes over. Return a specific finish reason rather than a generic one — it is what an operator sees in the board.

Pass phase="early" for planner/reflector-style nodes that must run before capability and watchdog gates and without prompt assembly. The default, phase="main", gets the full dispatch treatment.

Handlers can replace core types

register_node_handler replaces an existing handler for that type. A recipe-local register.py can therefore override a core node type for the rest of the process. Only load recipes you trust; read their register.py.

Implementer submodes

When one implementer node fans out into many independent units, register the dispatcher as data instead of code:

from mini_ork.cli.execute_handlers import register_implementer_submode

register_implementer_submode(
    recipe="docs-audit",
    node_id="implementer",
    results_artifact="units-results.json",
    script="scripts/run_units.py",
)

The script is invoked by the implementer handler and must write the declared results artifact. This keeps fan-out composition out of the executor.

Routing policies

A routing policy chooses a lane for a node:

from mini_ork.dispatch.routing import RoutingContext, register_policy

def route_by_risk(ctx: RoutingContext) -> str:
    if ctx.node_type in ("planner", "reviewer"):
        return "opus_lens"
    return "cheap_lens"

register_policy("by_risk", route_by_risk)
export MO_ROUTING_POLICY=by_risk

RoutingContext carries node_type, current_lane, root and task_class. The policy must return a lane name that exists in providers.yaml; an unknown lane should fail loudly rather than fall through to a default. Built-in policies include workflow_default, frontier_only, cheap_only, static_hybrid, learning_governed (the default) and trace_governed.

Gate evaluations

A gate type is a function from the gate's condition and context to a verdict:

from mini_ork.gates.gate_registry import register_gate_evaluator

def eval_file_exists(condition, context_json, db_path, mini_ork_root):
    # condition is the gate's own string; return "pass" | "fail" | "defer"
    import json, os
    ctx = json.loads(context_json or "{}")
    path = ctx.get("path")
    return "pass" if path and os.path.exists(path) else "fail"

register_gate_evaluator("file_exists", eval_file_exists)

The type is Callable[[str, str, str, Optional[str]], str] — condition, context JSON, DB path, engine root. Return exactly pass, fail or defer. An unregistered gate type keeps the historical fail-safe: it defers. That is the right default: an unknown gate must not silently pass.

Write the gate's context from the node that produced the artifact. A gate that cannot read its evidence should defer, not guess. See Verification and gates for how verdicts aggregate.

Providers and dispatch backends

providers.yaml entries name a kind. A new kind supplies a builder that turns configuration into a ProviderSpec:

from mini_ork.dispatch.providers import ProviderSpec, register_provider_kind

def build_my_provider(name, cfg, **kwargs) -> ProviderSpec:
    return ProviderSpec(
        name=name,
        # base URL, auth headers, transport, model mapping, ...
    )

register_provider_kind("my_endpoint", build_my_provider)

A dispatch backend bypasses the standard transport for one model lane:

from mini_ork.dispatch.providers import register_dispatch_backend

def dispatch_via_queue(request, spec):
    ...
    return DispatchResult(...)

register_dispatch_backend("my_lane", dispatch_via_queue)

The backend sees a DispatchRequest and ProviderSpec and returns a DispatchResult. It is responsible for its own timeout and for reporting a structured failure rather than raising into the executor.

Credentials belong in the vault

A provider builder should receive secret names and resolve them through the vault, not receive literal keys from YAML. See Security.

Subcommands

from mini_ork.cli.main import register_subcommand

def handle(rest: list[str], root: str) -> int:
    ...
    return 0

register_subcommand("my-command", handle)

The handler receives the remaining argv and the resolved engine root, and returns a process exit code. Registration is per process; a durable CLI addition is a module referenced from the dispatcher's native-subcommand table, not a runtime monkey-patch.

Embedders

Semantic memory uses an embedder selected by name:

from mini_ork.memory.semantic import register_embedder_provider

def make_embedder():
    from my_pkg.embed import MyEmbedder
    return MyEmbedder()

register_embedder_provider("my-embedder", make_embedder)
export MO_EMBED_PROVIDER=my-embedder

Keep third-party imports inside the factory. The factory runs only when that provider is selected, so import mini_ork stays cheap and does not require the optional dependency.

Recipe-local registration

Place register.py beside a recipe's task_class.yaml:

recipes/invoice-audit/
  task_class.yaml
  workflow.yaml
  artifact_contract.yaml
  register.py          # imports and registers the extensions above
  prompts/

The loader imports it once per process, before the run, by file path. It does not swallow errors: a syntax error, an import error or any exception fails the run with a non-zero exit and the original exception chained as the cause. A missing register.py is a silent no-op.

This is the recommended way to ship an extension with a recipe. It keeps the registration next to the workflow that needs it and avoids a global import side effect.

Whole recipes without code

Most new work needs no Python at all. A recipe is three YAML files plus prompts:

recipes/<name>/
  task_class.yaml        # when this recipe applies
  workflow.yaml          # nodes, edges, lanes, gates
  artifact_contract.yaml # required outputs and their checks
  prompts/<node>.md

Then:

mini-ork validate        # static checks before a run
mini-ork run <name> ./kickoff.md

Schemas and field meanings are in Schemas and Recipes and task classes.

Testing an extension

  • Unit-test a node handler by constructing a NodeDispatch with a tmp_path run dir; do not shell out to the whole executor.
  • Test a routing policy as a pure function over RoutingContext.
  • Test a gate evaluator by passing a context JSON and asserting the returned string is one of pass, fail, defer.
  • Integration-test a recipe with mini-ork validate and a dry run before a live run.

See Testing for fixtures, CI shards and the environment-hygiene rules the suite depends on.

Next