npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@odla-ai/workflows

v0.1.1

Published

Effect-safe helpers for Cloudflare Workflows: deterministic identities, idempotency boundaries, checkpoint validation, and a portable outbox contract and reconciler.

Readme

@odla-ai/workflows

⚠️ Experimental — an agentic-coding experiment. APIs can change before 1.0. Exercise the failure paths of your recipient and storage adapter before relying on this package for production mutations.

Effect-safety primitives for Cloudflare Workflows: replay- stable workflow/effect identities, canonical input fingerprints, an explicit idempotency boundary, checkpoint validation, and a portable outbox reconciler. The package uses only Web-standard APIs and does not import Node built-ins or cloudflare:workers.

The guarantee, precisely

This package does not promise exactly-once execution. Cloudflare can retry a step, a Worker can stop after a remote service accepts a request, and an outbox lease can expire before the success checkpoint is written. Those cases can all produce another delivery attempt.

What the package provides is:

  • deterministic IDs and SHA-256 fingerprints for plain JSON-like inputs;
  • the same idempotency key on every retry of one logical effect;
  • an adapter boundary that always supplies that key alongside the input;
  • bounded Workflow retry/timeout policy;
  • runtime rejection of lossy checkpoint values (undefined, NaN, dates, class instances, sparse arrays, accessors, cycles, and so on);
  • enforcement of Cloudflare's 1 MiB non-stream checkpoint ceiling;
  • atomic-lease semantics for portable outbox storage adapters; and
  • bounded reconciliation with retry scheduling and dead-letter state.

Your side-effect adapter must propagate idempotencyKey using the remote provider's idempotency header or field, and that provider must actually dedupe it. Without recipient-side deduplication, retries are at-least-once and can duplicate the mutation.

Install

npm view @odla-ai/[email protected] version
npm install --save-exact @odla-ai/[email protected]

Require the exact-version lookup to succeed first. An E404 means this release is unavailable; it does not prove the package name itself is absent.

A replay-safe Workflow effect

import { WorkflowEntrypoint, type WorkflowEvent, type WorkflowStep } from "cloudflare:workers";
import {
  asWorkflowStep,
  createWorkflowIdentity,
  runIdempotentEffect,
} from "@odla-ai/workflows";

interface SignupPayload {
  userId: string;
  email: string;
}

export class SignupWorkflow extends WorkflowEntrypoint<Env, SignupPayload> {
  async run(event: WorkflowEvent<SignupPayload>, step: WorkflowStep) {
    const identity = await createWorkflowIdentity("signup", event.payload, 1);

    return runIdempotentEffect(
      asWorkflowStep(step),
      {
        workflowId: identity.workflowId,
        name: "welcome-email",
        input: { userId: event.payload.userId, email: event.payload.email },
        config: {
          retries: { limit: 4, delay: "5 seconds", backoff: "exponential" },
          timeout: "1 minute",
        },
      },
      {
        execute: async ({ input, idempotencyKey, attempt }) => {
          const response = await fetch("https://mailer.example/send", {
            method: "POST",
            headers: {
              "content-type": "application/json",
              "idempotency-key": idempotencyKey, // required recipient boundary
            },
            body: JSON.stringify(input),
          });
          if (!response.ok) throw new Error(`mailer rejected request (${response.status})`);
          console.log("welcome email accepted", { attempt });
          const body = await response.json<{ requestId: string }>();
          return { requestId: body.requestId }; // plain checkpoint-safe data
        },
      },
    );
  }
}

asWorkflowStep is a type-only bridge. Cloudflare's recursive serializability generic can otherwise overflow TypeScript when composed with rich application types; no runtime wrapper is introduced.

The effect config intentionally accepts a fixed retry delay rather than a dynamic delay callback. Numeric delays/timeouts are milliseconds. Retry limits are checked against Cloudflare's 10,000 ceiling, timeout values above 30 minutes are rejected, and the optional sensitive: "output" flag is passed through.

name + version + canonical input identifies a workflow. workflowId + effect name + canonical effect input identifies an effect. Increment the workflow version when a semantic change must create new identities. Do not put random values or replay-time timestamps into identity inputs unless they are part of the original durable request.

Checkpoint-safe data

The accepted data model is finite JSON data: null, booleans, finite numbers, strings, dense arrays, and plain objects with enumerable data properties. Its serialized UTF-8 representation must fit within 1 MiB. Named TypeScript interfaces are accepted; validation happens before a side effect and again on the entire receipt before its result is checkpointed.

Invalid data throws CheckpointValueError, whose path identifies the failing value. Use assertCheckpointValue at additional persistence boundaries when a value does not pass through the built-in effect or outbox helpers.

import { checkpoint, fingerprint } from "@odla-ai/workflows";

const value = checkpoint({ z: 2, a: { ok: true } });
await fingerprint(value); // same hash if object keys arrive in another order

This is deliberately narrower than Cloudflare's full structured-clone and stream support so identities remain portable and canonical. Store large data in R2 and checkpoint a reference. See Cloudflare's Workflow limits.

The hash prevents cleartext payloads from appearing in IDs, but it is not an encryption or secrecy mechanism. Low-entropy inputs can be guessed. An idempotency key is an identifier, not an authorization credential.

Retry defaults

DEFAULT_EFFECT_CONFIG permits three total attempts with five-second exponential delay and a two-minute timeout. Pass an explicit config to runIdempotentEffect when the recipient needs a different bound.

DEFAULT_OUTBOX_RETRY permits eight delivery attempts, beginning at five seconds and doubling to a one-hour cap. retryDelay(attempt, policy) exposes the same deterministic, jitter-free calculation for storage adapters and operational tooling. Jitter, if desired, belongs in a durable schedule chosen once—not random data recomputed during Workflow replay.

Durable intent and reconciliation

When a mutation also changes local state, persist the outbox intent before delivery. For a true transactional outbox, your OutboxStore implementation must insert the intent in the same D1/SQL transaction as the corresponding domain change. Passing a transaction-scoped store to enqueueOutbox makes that possible; the library cannot create a cross-service transaction for you.

import {
  enqueueOutbox,
  reconcileOutbox,
  type IdempotentDispatcher,
  type OutboxStore,
} from "@odla-ai/workflows";

type EmailIntent = { to: string; template: string };
type EmailReceipt = { providerId: string };

declare const store: OutboxStore<EmailIntent, EmailReceipt>; // D1/DO adapter

await enqueueOutbox(store, {
  workflowId,
  effectName: "welcome-email",
  topic: "email.send",
  payload: { to: "[email protected]", template: "welcome" },
  now: requestedAtMs, // explicit durable input, not a hidden identity component
});

const dispatcher: IdempotentDispatcher<EmailIntent, EmailReceipt> = {
  async deliver({ payload, idempotencyKey, leaseExpiresAt }) {
    const remainingMs = leaseExpiresAt - Date.now() - 1_000; // safety margin
    if (remainingMs <= 0) throw new Error("outbox lease budget exhausted");
    return mailer.send(payload, {
      idempotencyKey, // required recipient boundary
      signal: AbortSignal.timeout(remainingMs),
    });
  },
};

const report = await reconcileOutbox({
  store,
  dispatcher,
  owner: `worker:${colo}`,
  leaseMs: 60_000,
  retry: {
    maxAttempts: 8,
    initialDelayMs: 5_000,
    maxDelayMs: 3_600_000,
    multiplier: 2,
  },
});

Run reconciliation from a scheduled Worker, queue consumer, alarm, or separate Workflow. A delivery crash window looks like this:

claim intent → remote accepts key K → worker stops → lease expires
             → claim again → remote receives K again → recipient dedupes K

The result is eventually recorded as delivered only after complete wins the current lease. After maxAttempts, the record becomes dead-letter; operators must inspect and explicitly replay or resolve it. Production adapters can implement RecoverableOutboxStore.requeueDeadLetter for an atomic replay that preserves the original recipient idempotency key and resets the attempt budget. Audit the failure before requeueing; the active row's lastError is cleared.

Implementing OutboxStore

The interface is intentionally storage-neutral. A D1, Durable Object, or SQL adapter must provide these atomic operations:

  1. putIfAbsent inserts by deterministic effect ID, returning the existing record on conflict.
  2. claim atomically selects due pending records (or expired in-flight records), increments attempts, and assigns a unique lease token.
  3. complete, retry, and deadLetter use compare-and-set semantics on (id, leaseToken) and reject an expired lease by returning false.
  4. Every state transition and its new timestamps are committed atomically.
  5. If supported, requeueDeadLetter atomically checks terminal state, preserves the identity/idempotency key, clears the active error, and resets attempts.

InMemoryOutboxStore implements those state rules for unit tests and local single-process use, including the optional dead-letter requeue contract. It is not durable, not distributed, and must not be used as a production outbox.

import { InMemoryOutboxStore } from "@odla-ai/workflows";

const testStore = new InMemoryOutboxStore<EmailIntent, EmailReceipt>();

Operational guidance

  • Alert on deadLettered > 0, sustained retries, and leaseLost > 0.
  • Keep dead-letter recovery on an authenticated, audited operator plane; never expose requeueDeadLetter to untrusted application callers.
  • The safe default claims one record. With an explicitly larger sequential batch, set the lease longer than the worst-case duration of the entire batch, not just one recipient request.
  • Bound recipient I/O against leaseExpiresAt with a safety margin. A request that outlives its lease can overlap a later delivery of the same stable key.
  • Keep batches bounded; the hard interface limit is 1000.
  • Return only checkpoint-safe receipts, never SDK response/class instances.
  • A storage exception rejects the reconciliation call. Already claimed rows remain in-flight until their leases expire; alert, retry the reconciler, and keep recipient-side idempotency enabled during recovery.
  • The default stored error is deliberately generic. Provide describeError only for a scrubbed, low-cardinality error code; never persist raw provider errors that may contain tokens or customer data.
  • Preserve outbox rows long enough for audit and manual recovery; define your own delivered/dead-letter retention policy in the storage adapter.

Relationship to @odla-ai/kg

@odla-ai/kg retains lightweight step policies for its existing pipelines. Use this package at mutation boundaries that need deterministic identities, recipient idempotency, or durable outbox recovery. It is deliberately generic and has no dependency on the graph, database, AI, or observability packages.