JS Workflows

Workflows orchestrate activities (and other workflows). They must be deterministic — given the same inputs and event history they always produce the same sequence of calls. Obelisk records every call and its result; on crash-recovery it replays the log, skipping already-completed calls. See Workflows for the general model.

[[workflow_js]]
name        = "my_workflow"
location    = "${DEPLOYMENT_DIR}/workflow/my_workflow.js"
ffqn        = "myapp:demo/workflow.my-workflow"
params      = []
return_type = "result<string, string>"

Use obelisk.sleep for delays instead of setTimeout. Date.now() and Math.random() are safe to use — their values are recorded in the execution log on first call and replayed deterministically on recovery.

obelisk.call — submit and await

Submits a child execution (activity or workflow) and blocks until it completes. Returns the ok payload directly; throws the err payload string if the child permanently fails.

// ffqn: myapp:demo/workflow.serial() -> result<string, string>
export default function serial() {
  let acc = 0;
  for (let i = 0; i < 10; i++) {
    obelisk.sleep({ seconds: 1 });
    const result = obelisk.call("myapp:demo/activity.step", [i, i * 200]);
    acc += Number(result);
    console.log(`step(${i})=${result}`);
  }
  return String(acc);
}

obelisk.sleep — persistent sleep

Pauses the workflow durably — the sleep position is saved to the execution log. If the server crashes mid-sleep and restarts, the sleep resumes where it left off.

obelisk.sleep({ milliseconds: 300 });
obelisk.sleep({ seconds: 1 });
obelisk.sleep({ minutes: 5 });

Join sets — parallel submission

Join sets let you submit multiple child executions concurrently and await their results individually.

// ffqn: myapp:demo/workflow.parallel() -> result<string, string>
export default function parallel() {
  const handles = [];
  for (let i = 0; i < 10; i++) {
    const js = obelisk.createJoinSet(); // optional: { name: "my-set" }
    const execId = js.submit("myapp:demo/activity.step", [i, i * 200]);
    handles.push({ i, js, execId });
  }
  let acc = 0;
  for (const { i, js, execId } of handles) {
    const response = js.joinNext(); // blocks until next result in this join set
    if (!response.ok) throw `step ${i} failed`;
    const result = obelisk.getResult(response.id); // { ok: value } or { err: value }
    acc = 10 * acc + Number(result.ok);
    obelisk.sleep({ milliseconds: 300 });
  }
  return String(acc);
}

Join set API:

CallReturnsDescription
let js = obelisk.createJoinSet()join set objectCreate a new join set (optionally { name: "…" })
js.submit(ffqn, argArray)childExecId (string)Submit a child execution without blocking
js.submitDelay(duration)delayId (string)Submit a timer (e.g. { milliseconds: 500 }). Duration key is one of: milliseconds/seconds/minutes/hours/days
js.joinNext(){ ok: bool, id: childExecId / delayId, type: "execution"/"delay" }Block until the next result in this join set
js.joinNextTry(){ ok: bool, id: childExecId / delayId, type: "execution"/"delay" } or { status: "allProcessed"/"pending" }Attempt to get next response without waiting if no response arrived yet
js.close()Cancel activities and delays, await child workflows
obelisk.getResult(childExecId){ ok/err: value }Fetch a result after child execution was submitted and consumed with joinNext*

Random values and time

Math.random() and Date.now() are safe to use in workflow code. Their values are recorded in the execution log on first execution and replayed identically on crash-recovery, preserving determinism.

const rand = Math.random(); // deterministic on replay
const now = Date.now(); // deterministic on replay

Obelisk also provides explicit workflow-safe helpers:

const n = obelisk.randomU64(0, 100); // u64 in [0, 100)
const n2 = obelisk.randomU64Inclusive(1, 6); // u64 in [1, 6]
const s = obelisk.randomString(8, 16); // alphanumeric, length in [8, 16)

obelisk.schedule — fire-and-forget submission

Schedules a new top-level execution without blocking the workflow. Returns immediately; the scheduled execution runs independently.

const execId = obelisk.executionIdGenerate();
obelisk.schedule(execId, "myapp:demo/activity.send-email", ["user@example.com"]);
// optional schedule-at: obelisk.schedule(execId, ffqn, args, { seconds: 60 });

obelisk.stub — inject a result for a stub activity

Stub activities have no implementation — the result is supplied externally (via the CLI, Web UI, or obelisk.stub). This enables human-in-the-loop workflows and test scenarios where a child result is provided without executing real code.

const js = obelisk.createJoinSet();
const execId = js.submit("myapp:stubs/approval.approve", [requestId]);
obelisk.stub(execId, { ok: "approved" }); // inject result (idempotent for same value)
js.joinNext();
const result = obelisk.getResult(execId).ok; // "approved"