Steps and Scripts

A step is one named, durable unit of workflow work. Good step boundaries make failures, retries, terminal output, and recovery understandable. If three actions must be observed and retried independently, they should usually be three steps rather than one very large script.

Write a step

WOML
<step id="calculateTotal" name="Calculate total" description="Adds tax to the order.">
  <script>
    const subtotal = context.payload.subtotal;
    return { total: subtotal * 1.2 };
  </script>
</step>

id is required and becomes both the durable node identity and the output path context.steps.calculateTotal. name and description make runtime output understandable to operators. Retry attributes define bounded attempts and are covered separately.

A fundamental step contains exactly one <script>. Do not put arbitrary operation tags beside the script or use a script attribute for timeout; workflow-wide timeout belongs in <config>.

Script behavior

The script body is the body of an asynchronous JavaScript function. Use statements, functions, await, conditions, loops, supported Bun imports, native fetch(), and return without adding a function wrapper.

WOML
<script>
  const response = await services.http.request({
    method: "GET",
    url: `https://api.example.com/orders/${context.payload.orderId}`
  }, { name: "load-order" });

  if (response.status !== 200) {
    throw new Error(`Unexpected order status ${response.status}`);
  }

  return { order: response.data };
</script>

WOML is XML-like but preserves script content as raw JavaScript. Do not use CDATA and do not escape <, >, or && inside the body. The first literal </script> ends the body, even when it appears inside a JavaScript string or comment.

Available bindings

Ordinary scripts receive:

BindingPurpose
contextRead the trigger payload and visible durable outputs.
attemptRead the current attempt, maximum attempts, and stable idempotency key.
servicesUse built-in managed capabilities and imported modules.
secretsRead only literal secret names proven during compilation.
fetchUse Bun's standard native Fetch implementation.
consoleWrite redacting logs to the workflow terminal.

Lifecycle and reusable definitions receive additional scope-specific bindings. Binding objects are read-only, and worker globals do not persist between invocations.

Return durable JSON

Valid results include null, booleans, strings, finite safe numbers, arrays, and plain objects. Do not return undefined, BigInt, functions, circular objects, clients, streams, or class instances.

The successful return is validated and durably recorded before later work starts:

JavaScript
return {
  orderId: context.payload.orderId,
  approved: true
};

If a step throws, later sequential steps do not run. The attempt failure is recorded, retry policy may schedule another attempt, and exhausted failure settles the workflow unless the failure occurs inside a control-flow structure with different reviewed behavior.

Choose useful step boundaries

Prefer a step for work that deserves its own name, result, retry identity, failure visibility, or operational timing. Keep small pure calculations together when splitting them would add noise without improving control.

Avoid hiding an entire workflow inside one script. A 500-line script can execute, but WOML can no longer show which business operation is running, retry only the failed part, or expose meaningful intermediate results.

Use Retries and Idempotency before retrying effectful work.