Temporal Workflows skill for Codex

Durable flows that replay cleanly after every deploy. Codex keeps Temporal workflow code deterministic and pushes every side effect into idempotent activities with explicit timeouts and bounded retries. Business failures become non-retryable errors that trigger saga compensation, long-lived loops continue as new, and any change to a workflow with running executions is patched and checked by replay tests against real histories. Covers TypeScript, Python and Go.

  • Backend & APIs
  • $temporal-workflows
  • 209 lines
  • 3 files
  • ~2,250 tokens
  • MIT
  • Updated
  • Editor's pick

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- temporal-workflows

Installs to ~/.agents/skills/temporal-workflows/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $temporal-workflows, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Designs and implements durable workflows on Temporal with the TypeScript, Python or Go SDK - deterministic workflow code, idempotent activities with start-to-close and heartbeat timeouts and retry policies, signals, queries and updates, sagas with compensation, continue-as-new for long histories, patching for safe deploys, and time-skipping plus replay tests. Use when building order, payment, onboarding or provisioning flows that must survive crashes, when a worker hits a non-determinism error, when activities retry forever or time out, or when changing a workflow that has running executions.

Use it when

  • Building multi-step business processes that must survive crashes and restarts
  • Activities retry forever, time out unexpectedly or double-charge on retry
  • A worker reports non-determinism after a code change
  • Workflows run for days or loop indefinitely and histories grow large

Use something else when

  • Simple fire-and-forget background tasks on a queue are enough: use background-jobs
  • You only need the saga concept without a workflow engine: use saga-pattern

How to check the result

Time-skipping tests cover success, compensation on a declined charge and approval timeout, and replay tests over downloaded production histories pass with the new code.

The mistake it prevents

Reordering or adding activity calls in a workflow with open executions without patching, which makes those executions fail with non-determinism errors on replay.

~/.agents/skills/temporal-workflows/SKILL.md

--- name: temporal-workflows description: Designs and implements durable workflows on Temporal with the TypeScript, Python or Go SDK - deterministic workflow code, idempotent activities with… license: MIT ---

Done well means: workflow code is pure orchestration and replays identically, every side effect lives in an idempotent activity with explicit timeouts and a bounded retry policy, business failures are modelled as non-retryable errors with compensation, histories stay small through continue-as-new, and every change to workflow logic passes replay tests against real histories before it ships.

Before changing anything

  1. Read AGENTS.md, the SDK and version (@temporalio/*, temporalio for Python, go.temporal.io/sdk), worker setup, task queue names and namespace configuration.
  2. Ask whether executions of the workflow are currently running in production. If yes, any change to command order needs versioning: read references/determinism.md before editing.
  3. Start a local server for development: temporal server start-dev (gRPC on 7233, Web UI on 8233). Installing the CLI needs network.
  4. Never cancel, terminate, reset or signal production executions without explicit user confirmation; these change real business processes.

What goes where

CodeLives inRules
Sequencing, branching, waiting, timers, stateWorkflowDeterministic; no I/O, no clocks, no randomness outside SDK APIs
HTTP calls, DB writes, email, payments, file I/OActivityIdempotent; may run more than once
Input to startWorkflow ID + argsWorkflow ID = business key (order-<id>) so duplicate starts are rejected
Large blobsObject storage; pass a referencePayloads are capped (about 2 MB each by default) and bloat history

Workflow and activities (TypeScript)

// activities.ts — normal Node code
export async function chargeCard(input: { orderId: string; amountCents: number }): Promise<string> {
  // Pass the idempotency key to the provider so a retried activity does not double charge.
  return payments.charge({ ...input, idempotencyKey: `charge-${input.orderId}` });
}
export async function refundCard(chargeId: string): Promise<void> { await payments.refund(chargeId); }
export async function reserveStock(orderId: string): Promise<void> { /* ... */ }
export async function releaseStock(orderId: string): Promise<void> { /* ... */ }
// workflows.ts — bundled into the workflow sandbox; import activity TYPES only
import { proxyActivities, defineSignal, defineQuery, setHandler, condition, ApplicationFailure } from '@temporalio/workflow';
import type * as activities from './activities';

const { chargeCard, refundCard, reserveStock, releaseStock } = proxyActivities<typeof activities>({
  startToCloseTimeout: '30 seconds',
  retry: {
    initialInterval: '1 second',
    backoffCoefficient: 2,
    maximumInterval: '1 minute',
    maximumAttempts: 10,
    nonRetryableErrorTypes: ['CardDeclined', 'InvalidOrder'],
  },
});

export const approveSignal = defineSignal<[boolean]>('approve');
export const statusQuery = defineQuery<string>('status');

export async function orderWorkflow(order: { id: string; amountCents: number }): Promise<string> {
  let status = 'reserving';
  let approved: boolean | undefined;
  setHandler(approveSignal, (ok) => { approved = ok; });
  setHandler(statusQuery, () => status);

  const compensations: Array<() => Promise<void>> = [];
  try {
    await reserveStock(order.id);
    compensations.unshift(() => releaseStock(order.id));

    status = 'awaiting-approval';
    if (!(await condition(() => approved !== undefined, '24 hours')) || !approved) {
      throw ApplicationFailure.nonRetryable('Order not approved', 'NotApproved');
    }

    status = 'charging';
    const chargeId = await chargeCard({ orderId: order.id, amountCents: order.amountCents });
    compensations.unshift(() => refundCard(chargeId));

    status = 'done';
    return chargeId;
  } catch (err) {
    status = 'compensating';
    for (const undo of compensations) await undo();
    throw err;
  }
}

If the workflow can be cancelled, run compensations inside CancellationScope.nonCancellable(...) so they still execute after cancellation.

Worker and client:

const worker = await Worker.create({
  connection: await NativeConnection.connect({ address: 'localhost:7233' }),
  taskQueue: 'orders',
  workflowsPath: require.resolve('./workflows'),
  activities,
});
await worker.run();

await client.workflow.start(orderWorkflow, {
  taskQueue: 'orders',
  workflowId: `order-${order.id}`,     // business key: duplicates fail with WorkflowExecutionAlreadyStarted
  args: [order],
});

Python and Go equivalents

@workflow.defn
class OrderWorkflow:
    @workflow.run
    async def run(self, order: Order) -> str:
        await workflow.execute_activity(
            reserve_stock, order.id,
            start_to_close_timeout=timedelta(seconds=30),
            retry_policy=RetryPolicy(maximum_attempts=10, non_retryable_error_types=["InvalidOrder"]),
        )
        ...
ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
    StartToCloseTimeout: 30 * time.Second,
    RetryPolicy: &temporal.RetryPolicy{MaximumAttempts: 10, NonRetryableErrorTypes: []string{"InvalidOrder"}},
})
if err := workflow.ExecuteActivity(ctx, ReserveStock, order.ID).Get(ctx, nil); err != nil {
    return "", err
}

Python workflows run in a sandbox: import activities and libraries inside with workflow.unsafe.imports_passed_through():. Go workflows must use workflow.Go, workflow.Channel and workflow.Selector, never native goroutines or select.

Timeouts and retries

SettingSet it toNotes
startToCloseTimeoutMax time one attempt can take, with marginRequired (or scheduleToClose); detects crashed workers
scheduleToCloseTimeoutTotal budget across all retriesUse when the business has a deadline
heartbeatTimeoutA few heartbeat intervalsLong activities (over ~1 min) heartbeat with progress details
maximumAttemptsBounded (5-20) or a schedule-to-close budgetThe default retry policy retries forever
nonRetryableErrorTypesBusiness failures (declined, invalid)Retrying a declined card is pointless

Activities are at-least-once: a worker can crash after the side effect but before reporting success. Use provider idempotency keys, upserts, or check-then-act on a unique business key.

Signals, queries, updates

NeedUse
Fire-and-forget input (approval, cancel request)Signal
Read current state, no side effectsQuery (must not mutate state or call activities)
Input that needs a validated result returned to the callerUpdate (with an optional validator to reject early)

Register handlers at the start of the workflow function so early signals are not lost.

Long-running workflows

  • Event histories have hard limits (about 50k events or 50 MB); performance degrades well before that. Loops that run indefinitely (subscriptions, polling, entity workflows) call continueAsNew with compact state, for example when workflowInfo().continueAsNewSuggested is true or every N iterations.
  • Drain signal handlers and wait for in-flight updates before continuing as new.
  • Use child workflows to partition large fan-outs (thousands of activities) rather than one huge history.

Testing

  • Unit-test activities as plain functions.
  • Workflow tests use the time-skipping environment so sleep('24 hours') takes milliseconds: TypeScript TestWorkflowEnvironment.createTimeSkipping(), Python WorkflowEnvironment.start_time_skipping(), Go testsuite.WorkflowTestSuite with mocked activities.
  • Test the unhappy paths: declined charge triggers compensation, approval timeout, worker restart mid-workflow (replay).
  • Replay tests against downloaded production histories run in CI on every change to workflow code; see references/determinism.md.

Checklist

  • Workflow code free of I/O, clocks, randomness and native concurrency
  • Every activity idempotent with start-to-close timeout and bounded retries
  • Business errors non-retryable; compensations registered after each step succeeds
  • Workflow ID is a business key; payloads small
  • Changes to running workflows patched; replay tests pass
  • Time-skipping tests cover success, failure with compensation, and timeouts

Gotchas

  • Importing an activity implementation (not just its type) into a TypeScript workflow file pulls Node modules into the sandbox bundle and fails the worker at startup.
  • Changing an activity’s function name renames its activity type and breaks running executions.
  • Catching all errors in a workflow and swallowing them hides cancellation; rethrow CancelledFailure (or check isCancellation).
  • A workflow task timeout loop usually means blocking code or a deadlock in workflow code, not a slow activity; check the worker logs for the stack.

What the zip contains

Codex reads bundled files only when a step points to them. Every one is shown here, so you can review it before installing.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Temporal Workflows"
  short_description: "Durable flows that replay cleanly after every deploy"
  default_prompt: "$temporal-workflows Build an order workflow in Temporal that reserves stock, waits for approval and charges the card"
references/determinism.md106 lines, 5.0 KB
# Determinism and versioning by SDK

Read this when writing or changing workflow code, when a replay test fails with a
non-determinism error, or when patching a workflow that has running executions.

## Why it matters

A worker rebuilds workflow state by re-running the workflow function against the stored
event history. Each command the code issues (schedule activity, start timer, start child)
must match the history in type and order. Anything that can differ between the original
run and a replay breaks that match.

## Forbidden in workflow code, and the replacement

| Do not use | TypeScript | Python | Go |
|---|---|---|---|
| Wall clock | `Date.now()` is patched to workflow time; still prefer `workflowInfo()` or values passed in | `workflow.now()` | `workflow.Now(ctx)` |
| Sleep / timers | `sleep('5m')` from `@temporalio/workflow` | `await asyncio.sleep()` is intercepted; `workflow.sleep()` also works | `workflow.Sleep(ctx, d)`, `workflow.NewTimer` |
| Random, UUIDs | `Math.random()` is seeded per workflow; `uuid4()` from `@temporalio/workflow` | `workflow.random()`, `workflow.uuid4()` | `workflow.SideEffect` or pass values in |
| Network, DB, filesystem, env | Activities only | Activities only | Activities only |
| Threads / goroutines | Not available (sandboxed V8 isolate) | Do not create threads; asyncio tasks are fine | `workflow.Go`, `workflow.NewChannel`, `workflow.NewSelector`, never `go` / native channels / `select` |
| Iterating maps | Insertion-ordered, fine | Dicts ordered, fine | Go map iteration is random: sort keys first |
| Global mutable state | Isolate per workflow run, but do not rely on module state | Sandbox reloads modules; do not mutate globals | Avoid package-level vars |
| Logging | `log` from `@temporalio/workflow` (replay-aware) | `workflow.logger` | `workflow.GetLogger(ctx)` |

Python sandbox: import activity modules and third-party libraries through
`with workflow.unsafe.imports_passed_through():` so they are not re-imported per run.

TypeScript bundling: workflow code is bundled separately (`workflowsPath`). Importing a
Node module (`fs`, an HTTP client) into workflow files fails the bundle; keep activities in
separate files and import only their types (`import type * as activities`).

## Changes that are safe without versioning

- Activity implementation changes (activities are not replayed).
- Changing activity options such as timeouts or retry policy (applies to new schedules).
- Adding a signal, query or update handler.
- Changing code that runs after the point every open execution has reached is still risky;
  assume it is unsafe unless replay tests say otherwise.

## Changes that need versioning

Adding, removing or reordering activities, timers, child workflows or `condition` waits;
changing which branch issues commands; changing activity types or names.

### Patching

TypeScript:

```ts
import { patched, deprecatePatch } from '@temporalio/workflow';

if (patched('send-sms-before-email')) {
  await sendSms(order);
}
await sendEmail(order);
// Later, once no execution started before the patch is still open:
// deprecatePatch('send-sms-before-email'); then remove the old branch in a further deploy.
```

Python:

```python
if workflow.patched("send-sms-before-email"):
    await workflow.execute_activity(send_sms, order, start_to_close_timeout=timedelta(seconds=30))
await workflow.execute_activity(send_email, order, start_to_close_timeout=timedelta(seconds=30))
# later: workflow.deprecate_patch("send-sms-before-email")
```

Go:

```go
v := workflow.GetVersion(ctx, "send-sms-before-email", workflow.DefaultVersion, 1)
if v == 1 {
    if err := workflow.ExecuteActivity(ctx, SendSMS, order).Get(ctx, nil); err != nil {
        return err
    }
}
```

Patch lifecycle: (1) deploy with `patched`/`GetVersion` branch, (2) wait until all
executions that started on old code have closed (check with a visibility query), (3)
deprecate the patch, (4) remove the old branch. Skipping step 2 breaks those executions.

### Alternatives

- Short-lived workflows: drain the old task queue with old workers while new starts go to
  a new task queue or workflow type name (`OrderWorkflowV2`).
- Worker Versioning (deployment-based, pinned vs auto-upgrade behaviour) lets executions
  stay on the build they started with. Check your server and SDK versions support it before
  adopting; the API has changed across releases.

## Replay testing

Download real histories and replay them against the new code in CI:

```bash
temporal workflow show --workflow-id order-123 --output json > histories/order-123.json
```

- TypeScript: `await Worker.runReplayHistory({ workflowsPath: require.resolve('./workflows') }, history)`
- Python: `await Replayer(workflows=[OrderWorkflow]).replay_workflow(WorkflowHistory.from_json(wf_id, json_str))`
- Go: `replayer := worker.NewWorkflowReplayer(); replayer.RegisterWorkflow(OrderWorkflow); replayer.ReplayWorkflowHistoryFromJSONFile(logger, "histories/order-123.json")`

Keep a few histories per workflow type covering each branch, refresh them after each
intentional change, and fail the build on any non-determinism error.

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc