Workflow Quickstart
Edit a generated workflow safely, call platform-owned capabilities, and ship the current contract.
Pax workflows are typed JavaScript generators. A workflow receives a trigger-specific context, yields JSON-serializable commands, and resumes with the command result. Start from the current generated default in the Workflow Editor instead of recreating entry points or command types from documentation.
Make a first change
- Open the module in the Workflow Editor and reload its current default.
- Keep every generated entry point and edit the smallest relevant function body.
- Yield only commands offered by that module.
- Save and test in singleplayer.
This focused chat example uses the current context-provided beneficiary list and asks for the normalized response so the workflow can retain attempt identity:
function* onHumanMessage(ctx) {
var response = yield {
type: "callAIResult",
params: {
prompt: "Write a short in-character greeting for this game.",
slot: "game",
purpose: "conversation",
beneficiaryUserIds: ctx.beneficiaryUserIds
}
};
if (!response.ok) {
yield { type: "settleAIFailure", error: response.error };
return;
}
yield {
type: "log",
level: "info",
message: "AI response completed",
data: { semanticStatus: response.value.semanticStatus }
};
}The complete chat default owns message persistence and every required entry point. Use this body as a pattern, not as a replacement module.
Current compatibility boundary
Workflow compatibility is v5. The generated manifest and schema registry are the authority for module entry points, yield types, and command payloads. Older overrides remain visible but are flagged as stale until they are updated and saved again.
Runtime values are validated immediately before dispatch, so a payload that only looks correct statically is still rejected if it does not match its command schema at run time.
Sandbox rules
Workflow code cannot access fetch, provider SDKs, secrets, the filesystem, browser APIs,
process.env, globalThis, window, document, the substrate context, timers, or background
tasks. Use injected deterministic helpers such as paxNow() and paxRandom() rather than ambient
time or randomness. Every effect must complete through a yielded command before the run advances.
Normal imports are limited to pure helpers that the compiler can bundle into the hardened
compartment.
Advisor workflows
The advisor module has the same shape as every other AI-capable module. It has three entry points.
onAdvisorMessage answers a player's question: your workflow opens the stream with startAIStream,
declaring on params.tools whatever the model may draw, pulls events with getNextModelEvent,
paints them live with emitAdvisorDelta / emitAdvisorArtifact, and closes with
finalizeAdvisorReply, failAdvisorReply, or reportAIError. Retrying, giving up, what a tool call
means, and what a dropped chart means are all decisions your code makes.
getNextModelEvent hands you the model's normalized events: text.delta for prose, tool.started
when the model begins a tool call, tool.ready with arguments that already parsed and matched the
schema you declared, tool.invalid when they did not, and end. A tool call does nothing on its own
— turning one into an artifact is a line of your own code, so a chart the default workflow draws is a
chart you can change or remove. tool.started arrives before the arguments are written and carries
no data beyond a toolName the provider may have withheld; it exists so you can tell the player a
figure is coming, which for a large chart is seconds before there is anything to draw.
setAdvisorLoading { loading } says what the player is waiting for, and clearAdvisorLoading takes it
down again. The host shows nothing on its own: there is no generic spinner behind these commands, so
what the panel shows while an answer is being prepared is entirely what your workflow sets, and a
workflow that never calls them leaves it empty until the first delta lands. loading.type is
"message" for a chat bubble of its own or "content" to render inline at the point the reply has
reached — which is where an artifact emitted next will land, so a "content" indicator set on
tool.started is replaced by the chart it stood in for. loading.message is optional and is
translated for the player; loading.icon defaults to the spinner; loading.style picks a
presentation variant. It is ephemeral and is never written to the thread: nothing survives the run,
and a saved reply carries no trace of it. Both commands are turn-rail only.
emitAdvisorMessage is what saves a reply. Call it zero or more times — if you never call it, no
advisor message is ever saved, no matter how much text streamed past. The streaming commands above
only paint the screen. It also takes attachments: [{ type: "report", reportId }] to render a report
inline; the host silently drops any attachment naming a report that player cannot see.
savePlayerMessage records the player's own question in the thread. It takes no arguments on
purpose: the host writes the moderated text it already holds, so you choose whether the question is
recorded but never what it said. The approved text is on ctx.submittedText if you want it in your
prompt, which is where the default workflow gets it — so the prompt does not depend on the message
having been written. Skip the command and the advisor answers a question the thread never shows.
createAdvisorReport { title, body } writes a standing report and returns its reportId, which you
can then pass to emitAdvisorMessage's attachments. There is no audience argument: the report
belongs to the player whose thread this is, and the host derives that, so a workflow cannot author a
document visible to someone else. The shipped default does not create reports — this is here for
yours.
onJumpForwardComplete runs once per player after a jump forward commits, and decides what the
advisor says about it unprompted. Announcing nothing is a valid choice — reports stay reachable
from the advisor's own report list and the jump's event card, so a message here highlights a report
rather than delivering it. There is no turn on this entry point, so emitAdvisorMessage is the only
advisor command that applies and AI goes through callAI / callAIResult; startAIStream returns
{ ok: false }. The player's thread is created only if you emit, so a workflow that stays quiet
writes nothing at all.
onStopRequested fires when the player presses Stop, before the generation is torn down. It is a
chance to say a parting word, not a veto: whatever your code does, the host then cancels the run,
clears the waiting state and marks the turn cancelled. Stopping is the player's right and a workflow
cannot switch it off, slow it down, or refuse it — if your reaction parks on an AI call it is dropped
where it stands, and callAI is refused outright so pressing Stop can never cost the player money.
emitAdvisorMessage and createAdvisorReport work here; the streaming commands, savePlayerMessage
and the loading commands all return a refusal you can branch on. The shipped default does nothing
with it.
The chart tools are your workflow's own model tools, declared on the AI call like any others; the host validates them against the schemas you offered and hands the calls back as data. What the host still owns, exactly as it does for chat, is the player's own message, which is moderated and persisted before your workflow runs. Message ids, timestamps, and round stamps are stamped on append. Thread create, rename, delete, and switch are host WebSocket handlers with no workflow command.
Capability and moderation boundaries
- Moderation is pre-gated and cannot be bypassed. Supported human inputs pass through the platform gate before creator workflow invocation. Workflows cannot call moderation services, inspect or change policy, approve content, recover blocked drafts, or route the same input around the gate.
- No arbitrary client messages. A workflow yields only commands in its generated module contract. It cannot write WebSocket frames, invent message types, or reach the browser directly.
- No billing or wallet authority. Workflows declare beneficiaries. They cannot inspect or mutate balances, choose payers/resources/shares, grant credit, or call billing APIs.
- No platform UI authority. Workflows may use contracted gameplay presentation commands, but cannot synthesize SystemCards, SystemActionRefs, settings modals, moderation overlays, or terminal platform chrome.
- No service escape hatch. Workflow code never receives
c.api, credentials, route URLs, or provider clients. Module executors expose only their generated command contract.
These are capability boundaries enforced by the sandbox itself: the hardened SES Compartment's global object contains only the contracted game scope, so platform capabilities are unreachable rather than merely discouraged. Runtime schema validation checks every yielded value again.
Where overrides run
Custom workflows run in singleplayer only. Multiplayer games keep using the generated default, because workflow code is outside the moderation scan. Reload the current default after a compatibility change instead of preserving an older command shape with fallback branches.
AI guides
AI slots and tools
Choose utility, game, or advisor and consume normalized model output.
Beneficiaries and payers
Declare who benefits while the platform resolves payment.
Failures and retries
Settle opaque failures and make deliberate, separately billed retries.
Round compression
Let jump-forward decide when to request platform-owned compression.