Skip to content
Moku
@moku-labs/ai pages

How it works

One pipeline for every item: plan, reuse, gate, execute, persist. The journal and the store make it safe to kill.

On this page

The picture

  • *.moku.yaml
  • buildfile
  • runner
  • registry
  • provider handler
  • store, then journal
PartJob
buildfileCompiles the build files through one zod schema into a BuildSpec.
runnerPlans every item and drives it through the steps below.
registryFinds the handler for the item's task and provider.
Provider handlerCalls the service. Returns the bytes and the real cost.
storeKeeps the bytes.
journalRecords the hash and the cost, after the bytes are safe.

Tasks and providers never import each other. A task plugin owns a contract, a provider plugin registers a handler for it, and the runner only sees the handler.

One item, step by step

  1. Plan. Order the items so $ref targets come first. Resolve the provider. Hash every $file. Compute two keys. Insert the item as queued.
  2. Reuse. A done artifact with the same artifact key, from any run, with its bytes still in the store, completes the item at cost 0. No provider call.
  3. References. Wait for the $ref targets. A target that did not end done blocks the item: it stays queued and the run ends paused.
  4. Claim. One item per artifact key reaches the provider at a time, across all active runs.
  5. Admit. limits.acquire("{task}/{provider}/default") waits for a rate token and a concurrency slot.
  6. Gate. One SQLite transaction checks the budget, drops duplicates and moves the item queued → dispatching.
  7. Execute. The handler runs: execute, or submit and poll for a long job.
  8. Persist. store.put writes the bytes. Then journal.commitDone writes the metadata. An artifact exists only when both are there, bytes first.
  9. Report. A record goes to app.runner.events(). Progress goes to the plugin bus, at most once per 500 ms.

Two keys

KeyHash ofWhat it does
Planning keytask, input, paramsThe item's identity inside a run. $ref uses the target's planning key. Excludes provider.
Artifact keytask, provider, pack version, input, paramsThe identity of the result. $ref uses the target's artifact key. This key drives reuse.

Each $file enters both keys as its content hash. So one changed keyframe rebuilds that keyframe and every shot that references it. The other items are reused.

The id is not part of either key. It is the file name of the export and the target of a $ref.

Which provider runs

An item picks its provider in this order:

  1. provider on the item.
  2. defaults.provider in the build file.
  3. The first provider registered for the task.

The budget gate

The gate runs right before each provider call, in one transaction:

done actual cost + dispatching estimates + this item's estimate <= maxCostUsd

When the check fails, the run stops admitting, lets in-flight items finish and settles budget-stopped. The guarantee is that spend stays within the done items plus the items that were dispatching at a kill. estimate uses the same math, and moku estimate shows it.

Failures

Error classComes fromOutcome
http-5xxstatus >= 500Retry with backoff
http-429status === 429Retry with backoff. A longer Retry-After wins.
timeoutkind: "timeout"Retry with backoff
networkkind: "network"Retry with backoff
http-4xxOther 4xxfailed
content-policykind: "content-policy"flagged, never queued again
invalid-requestkind: "invalid-request"failed after one attempt
local-failurekind: "local-failure"failed after one attempt
unknownNo hint at allfailed after one attempt. A bug must not re-run a paid job.

Backoff is exponential in the attempt number, from runner.retryBaseMs, jittered to 50-100%. runner.maxAttempts caps the attempts. A build file can set defaults.maxAttempts.

limits counts retryable failures per lane. After breakerThreshold in a row the lane's breaker opens for breakerCooldownMs, and items wait.

Crashes, pauses and long jobs

The journal runs SQLite in WAL mode with synchronous=FULL. Every state change is a committed write, so a kill -9 loses only work in flight.

What happenedWhat the next run does
Crash or killmoku run or runner.resume() queues the dispatching items again. done items are never billed again.
Ctrl-C or an aborted signalThe run stops admitting and drains, then settles paused. Exit code 4.
app.stop()Pauses every active run and waits for it.
A submit + poll job was in flightThe job id was journaled before any wait, so the next attempt polls the same job. It does not pay twice.
A job passed jobTimeoutMsThe job is expired, and the next attempt polls it again first. A job that expired twice is not adopted again.

What is stored where

Journal (.moku/journal.db)Store (.moku/store/)
HoldsStatuses, costs, hashes, error classesThe artifact bytes
Never holdsPrompts, request bodies, keysMetadata
Pluginjournalstore

This split is the redaction boundary. A prompt cannot reach the journal, because the runner only passes it statuses, costs and hashes. A generated text output, like a translate result, is an artifact in the store.

Durable path or one-off call

app.runner.run(...)app.voiceover.generate(...)
Journaledyesno
Budget gateyesno
Resumableyesno
Reuses artifactsyesno
Good forAnything worth keepingScripts, tests, experiments
both.ts
import { app } from "./app";

await app.start();

// One-off: direct, not journaled.
const speech = await app.voiceover.generate({ text: "Hi!", voice: "21m00Tcm4TlvDq8ikWAM" });
console.log(speech.costUsd);

// Durable: journaled, budget-gated, resumable.
const result = await app.runner.run({ files: "**/*.moku.yaml", maxCostUsd: 25 });
if (result.status === "paused") await app.runner.resume({ runId: result.runId });

await app.stop();

The plugin order

createApp composes five core plugins and the regular plugins. Core plugins are on every plugin's ctx: log, env, journal, store, limits. Regular plugins mount at app.<name>. Providers register their handlers in onInit, so app.registry is full as soon as createApp returns, and the order of registration decides the third step of Which provider runs.

TaskProviders, in registration order
voiceoverelevenlabs, openai
translateopenai
prompt-genopenai, anthropic, codex, claude, fal
imagecodex, fal, ark
videofal, apimodels, ark
musicelevenlabs, fal
sfxelevenlabs, fal
spritefal
assetark