{"_id":"@abshahin/workflows-sdk","_rev":"5-fd46f996dbc1f39abeb585e58cf1193d","name":"@abshahin/workflows-sdk","dist-tags":{"latest":"0.1.5"},"versions":{"0.1.0":{"name":"@abshahin/workflows-sdk","version":"0.1.0","keywords":["cloudflare","cloudflare-workers","cloudflare-workflows","workflow","sdk","typescript","background-jobs"],"license":"MIT","_id":"@abshahin/workflows-sdk@0.1.0","maintainers":[{"name":"abshahin","email":"abdelrahmanshaheeen8@gmail.com"}],"homepage":"https://github.com/aashahin/workflows-sdk#readme","bugs":{"url":"https://github.com/aashahin/workflows-sdk/issues"},"dist":{"shasum":"3879e7c62b9ec2b45287374524472fbcb3825c4c","tarball":"https://registry.npmjs.org/@abshahin/workflows-sdk/-/workflows-sdk-0.1.0.tgz","fileCount":22,"integrity":"sha512-/zss3ySM31Zc7L3lABf5Z4LQ1GwVxZux+NsT1q/Kf6Ul58flxOBbR52XZiVpntR7AjN2I+GaJQ8L+ac6CoZ4qg==","signatures":[{"sig":"MEQCIAuDgpq1bKA4QKUtibqJkBl+LSK3tpc0Yiqz0QYUr15yAiAOBhILAsKhyYayz+3t7xlfX0kCBbOdzIYuhESuiT47Kg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":55121},"type":"module","exports":{".":"./src/index.ts","./helpers":"./src/helpers/index.ts","./contracts":"./src/contracts/index.ts"},"gitHead":"9d16226bd3149e98136780032873ed1eb27b3072","_npmUser":{"name":"abshahin","email":"abdelrahmanshaheeen8@gmail.com"},"repository":{"url":"git+https://github.com/aashahin/workflows-sdk.git","type":"git"},"_npmVersion":"10.9.3","description":"TypeScript SDK for dispatching typed workflow events to a Cloudflare Worker runtime.","directories":{},"_nodeVersion":"22.20.0","dependencies":{},"_hasShrinkwrap":false,"devDependencies":{"bun-types":"^1.3.8","typescript":"^5.9.3"},"peerDependencies":{},"_npmOperationalInternal":{"tmp":"tmp/workflows-sdk_0.1.0_1773895909868_0.0898135361278507","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@abshahin/workflows-sdk","version":"0.1.2","keywords":["cloudflare","cloudflare-workers","cloudflare-workflows","workflow","sdk","typescript","background-jobs"],"license":"MIT","_id":"@abshahin/workflows-sdk@0.1.2","maintainers":[{"name":"abshahin","email":"abdelrahmanshaheeen8@gmail.com"}],"homepage":"https://github.com/aashahin/workflows-sdk#readme","bugs":{"url":"https://github.com/aashahin/workflows-sdk/issues"},"dist":{"shasum":"a865a112ba600667bb2a5de4211d28d423ca1e19","tarball":"https://registry.npmjs.org/@abshahin/workflows-sdk/-/workflows-sdk-0.1.2.tgz","fileCount":24,"integrity":"sha512-xcbzKnjcAmAHAc7p6Hz6QouW91mFgIWREhZ+7t+uqeyO0xuOAvWYkNiq+MBguV0Dygko8SNwS8xApHQjVwoNEA==","signatures":[{"sig":"MEYCIQDhC9vMZmYrIb6tPqSdz+XBRtAHITk+Oxf0I0dQ0L01KQIhAPt3BoKGIwVsrTZJP0ojWxenXI7e79tzEpsLEn8ZzST2","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":59968},"type":"module","exports":{".":"./src/index.ts","./helpers":"./src/helpers/index.ts","./contracts":"./src/contracts/index.ts"},"gitHead":"36e701db9661048019d62deae4820b9886217a3e","_npmUser":{"name":"abshahin","email":"abdelrahmanshaheeen8@gmail.com"},"repository":{"url":"git+https://github.com/aashahin/workflows-sdk.git","type":"git"},"_npmVersion":"11.12.1","description":"TypeScript SDK for dispatching typed workflow events to a Cloudflare Worker runtime.","directories":{},"_nodeVersion":"25.9.0","dependencies":{},"_hasShrinkwrap":false,"devDependencies":{"bun-types":"^1.3.8","typescript":"^5.9.3"},"peerDependencies":{},"_npmOperationalInternal":{"tmp":"tmp/workflows-sdk_0.1.2_1778047780619_0.2656576557678465","host":"s3://npm-registry-packages-npm-production"}},"0.1.3":{"name":"@abshahin/workflows-sdk","version":"0.1.3","keywords":["cloudflare","cloudflare-workers","cloudflare-workflows","workflow","sdk","typescript","background-jobs"],"license":"MIT","_id":"@abshahin/workflows-sdk@0.1.3","maintainers":[{"name":"abshahin","email":"abdelrahmanshaheeen8@gmail.com"}],"homepage":"https://github.com/aashahin/workflows-sdk#readme","bugs":{"url":"https://github.com/aashahin/workflows-sdk/issues"},"dist":{"shasum":"b9f253d86c59a6e29c0ac066a84852e8ff1b2fb1","tarball":"https://registry.npmjs.org/@abshahin/workflows-sdk/-/workflows-sdk-0.1.3.tgz","fileCount":36,"integrity":"sha512-6jq1LFk9i3tBt8RKzYfx5FSqN3fwIel6OTnN4jgZXrUzueaOC6gXfC3vobU8dkinWiQ4xt0xFgwGtZ3XB+3Ztg==","signatures":[{"sig":"MEYCIQDgkZIQ6OfUT5H+w/u+49lY3LPAft/ixbZa+7+3pdRI/AIhAKkTKIw1xSbpc4W+dZ9DpFg/jykqymldnmk3UbnaD7O9","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":191283},"type":"module","types":"./src/index.ts","module":"./src/index.ts","exports":{".":{"types":"./src/index.ts","import":"./src/index.ts","default":"./src/index.ts"},"./bun":{"types":"./src/bun/index.ts","import":"./src/bun/index.ts","default":"./src/bun/index.ts"},"./http":{"types":"./src/http/index.ts","import":"./src/http/index.ts","default":"./src/http/index.ts"},"./testing":{"types":"./src/testing/index.ts","import":"./src/testing/index.ts","default":"./src/testing/index.ts"},"./scheduler":{"types":"./src/scheduler/index.ts","import":"./src/scheduler/index.ts","default":"./src/scheduler/index.ts"},"./cloudflare":{"types":"./src/cloudflare/index.ts","import":"./src/cloudflare/index.ts","default":"./src/cloudflare/index.ts"}},"gitHead":"d16f15cb7c085fb2eaa54b13de8305305cb0e7e1","_npmUser":{"name":"abshahin","email":"abdelrahmanshaheeen8@gmail.com"},"repository":{"url":"git+https://github.com/aashahin/workflows-sdk.git","type":"git"},"_npmVersion":"11.12.1","description":"Unified TypeScript SDK for typed workflows across Cloudflare Workflows and Bun runtimes.","directories":{},"_nodeVersion":"25.9.0","dependencies":{},"_hasShrinkwrap":false,"devDependencies":{"bun-types":"^1.3.8","typescript":"^5.9.3"},"peerDependencies":{},"_npmOperationalInternal":{"tmp":"tmp/workflows-sdk_0.1.3_1779721280184_0.0031167247338044213","host":"s3://npm-registry-packages-npm-production"}},"0.1.4":{"name":"@abshahin/workflows-sdk","version":"0.1.4","keywords":["cloudflare","cloudflare-workers","cloudflare-workflows","workflow","sdk","typescript","background-jobs"],"license":"MIT","_id":"@abshahin/workflows-sdk@0.1.4","maintainers":[{"name":"abshahin","email":"abdelrahmanshaheeen8@gmail.com"}],"homepage":"https://github.com/aashahin/workflows-sdk#readme","dist":{"shasum":"6f773ae4855d4798210112394a6eb2b1ee4f85ae","tarball":"https://registry.npmjs.org/@abshahin/workflows-sdk/-/workflows-sdk-0.1.4.tgz","fileCount":38,"integrity":"sha512-IkjT41/fR1MSn7ZvTMGEYsd58tgulxLfXpOy0LGtOrCiKYPTwJJwPVVN07tkcjM60yuXOviABwOFDN8QJfqkzg==","signatures":[{"sig":"MEYCIQDu3xVud4LIkXPtv8QeW2/ZziyhRBZ+Kcxin2vviaLN0gIhAJsdAfWnSgV0+P8C6AZwkBbwgcwRKPv1Kxk/o0Lwhq7W","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":211271},"type":"module","types":"./src/index.ts","module":"./src/index.ts","shasum":"6f773ae4855d4798210112394a6eb2b1ee4f85ae","exports":{".":{"types":"./src/index.ts","import":"./src/index.ts","default":"./src/index.ts"},"./bun":{"types":"./src/bun/index.ts","import":"./src/bun/index.ts","default":"./src/bun/index.ts"},"./http":{"types":"./src/http/index.ts","import":"./src/http/index.ts","default":"./src/http/index.ts"},"./testing":{"types":"./src/testing/index.ts","import":"./src/testing/index.ts","default":"./src/testing/index.ts"},"./scheduler":{"types":"./src/scheduler/index.ts","import":"./src/scheduler/index.ts","default":"./src/scheduler/index.ts"},"./cloudflare":{"types":"./src/cloudflare/index.ts","import":"./src/cloudflare/index.ts","default":"./src/cloudflare/index.ts"}},"_npmUser":{"name":"abshahin","email":"abdelrahmanshaheeen8@gmail.com"},"_integrity":"sha512-IkjT41/fR1MSn7ZvTMGEYsd58tgulxLfXpOy0LGtOrCiKYPTwJJwPVVN07tkcjM60yuXOviABwOFDN8QJfqkzg==","repository":{"url":"git+https://github.com/aashahin/workflows-sdk.git","type":"git"},"_npmVersion":"10.8.3","description":"Unified TypeScript SDK for typed workflows across Cloudflare Workflows and Bun runtimes.","directories":{},"_nodeVersion":"24.3.0","dependencies":{},"_hasShrinkwrap":false,"devDependencies":{"bun-types":"^1.3.8","typescript":"^5.9.3"},"peerDependencies":{},"_npmOperationalInternal":{"tmp":"tmp/workflows-sdk_0.1.4_1780202587878_0.020873711022041608","host":"s3://npm-registry-packages-npm-production"}},"0.1.5":{"name":"@abshahin/workflows-sdk","version":"0.1.5","type":"module","description":"Unified TypeScript SDK for typed workflows across Cloudflare Workflows and Bun runtimes.","module":"./src/index.ts","types":"./src/index.ts","license":"MIT","repository":{"type":"git","url":"git+https://github.com/aashahin/workflows-sdk.git"},"homepage":"https://github.com/aashahin/workflows-sdk#readme","keywords":["cloudflare","cloudflare-workers","cloudflare-workflows","workflow","sdk","typescript","background-jobs"],"exports":{".":{"types":"./src/index.ts","import":"./src/index.ts","default":"./src/index.ts"},"./bun":{"types":"./src/bun/index.ts","import":"./src/bun/index.ts","default":"./src/bun/index.ts"},"./cloudflare":{"types":"./src/cloudflare/index.ts","import":"./src/cloudflare/index.ts","default":"./src/cloudflare/index.ts"},"./http":{"types":"./src/http/index.ts","import":"./src/http/index.ts","default":"./src/http/index.ts"},"./scheduler":{"types":"./src/scheduler/index.ts","import":"./src/scheduler/index.ts","default":"./src/scheduler/index.ts"},"./testing":{"types":"./src/testing/index.ts","import":"./src/testing/index.ts","default":"./src/testing/index.ts"}},"peerDependencies":{},"dependencies":{},"devDependencies":{"bun-types":"^1.3.8","typescript":"^5.9.3"},"gitHead":"2be402a9af0230a6d931b61c16ac4764670ac593","_id":"@abshahin/workflows-sdk@0.1.5","bugs":{"url":"https://github.com/aashahin/workflows-sdk/issues"},"_nodeVersion":"25.9.0","_npmVersion":"11.12.1","dist":{"integrity":"sha512-abwWFfFWBsAMNBK3tcSjskfebrN10r2RE8tiHzrrNnnrP1kHILkeHqRnnYCMbQKZIu26zG+byxV5nuMsmykSWQ==","shasum":"ae806ec0a3e00b8f600805968b2b1e5cd675b08b","tarball":"https://registry.npmjs.org/@abshahin/workflows-sdk/-/workflows-sdk-0.1.5.tgz","fileCount":42,"unpackedSize":378629,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIAlUwHgl7B5ohoqI/G4NgiGm8hzUj4kxZ6bFvUh4ZaC3AiEAj9bQB/Pe+JrvtP+xC02zpB455Jczqrg/mmVcm8oosCc="}]},"_npmUser":{"name":"abshahin","email":"abdelrahmanshaheeen8@gmail.com"},"directories":{},"maintainers":[{"name":"abshahin","email":"abdelrahmanshaheeen8@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/workflows-sdk_0.1.5_1787884669861_0.10967895880754885"},"_hasShrinkwrap":false}},"time":{"created":"2026-03-19T04:51:49.763Z","modified":"2026-08-28T02:37:50.146Z","0.1.0":"2026-03-19T04:51:50.028Z","0.1.2":"2026-05-06T06:09:40.767Z","0.1.3":"2026-05-25T15:01:20.336Z","0.1.4":"2026-05-31T04:43:08.046Z","0.1.5":"2026-08-28T02:37:50.003Z"},"license":"MIT","homepage":"https://github.com/aashahin/workflows-sdk#readme","keywords":["cloudflare","cloudflare-workers","cloudflare-workflows","workflow","sdk","typescript","background-jobs"],"repository":{"type":"git","url":"git+https://github.com/aashahin/workflows-sdk.git"},"description":"Unified TypeScript SDK for typed workflows across Cloudflare Workflows and Bun runtimes.","maintainers":[{"name":"abshahin","email":"abdelrahmanshaheeen8@gmail.com"}],"readme":"# @abshahin/workflows-sdk\n\nRuntime-neutral TypeScript workflow definitions with adapters for Bun and Cloudflare Workflows.\n\nThe SDK gives you one typed workflow contract:\n\n- Define workflows once with `defineWorkflow()`.\n- Dispatch standard envelopes with `createWorkflowClient()`.\n- Run the same workflow definitions on Bun, a custom Cloudflare Worker endpoint, or Cloudflare's public Workflows REST API.\n- Persist idempotency, cron claims, workflow state, step results, retries, and dead letters through runtime adapters.\n\n## Installation\n\nInstall the package with your preferred JavaScript package manager:\n\n```json\n\"@abshahin/workflows-sdk\": \"^0.1.0\"\n```\n\nThe package exports TypeScript source files directly. Use it from runtimes and bundlers that can load TypeScript subpath exports, or compile it as part of your application build.\n\n## Exports\n\n| Import path | Purpose |\n| --- | --- |\n| `@abshahin/workflows-sdk` | Core workflow definition, registry, client, envelope, scheduler, and error types |\n| `@abshahin/workflows-sdk/http` | `SignedHttpAdapter` for a custom worker endpoint with `/dispatch` and `/status/:id` |\n| `@abshahin/workflows-sdk/cloudflare` | Cloudflare Worker dispatch handler, Workflow entrypoint helper, and REST API adapter |\n| `@abshahin/workflows-sdk/bun` | Bun runtime plus SQLite and Redis adapters |\n| `@abshahin/workflows-sdk/scheduler` | Cron helpers and cron definition types |\n| `@abshahin/workflows-sdk/testing` | In-memory adapter for tests |\n\n## Core API\n\nDefine workflow logic with a name, optional schema, optional cron definitions, optional retry/timeout defaults, and a `run()` function.\n\n```ts\nimport {\n  createWorkflowClient,\n  defineWorkflow,\n  defineWorkflowRegistry,\n} from \"@abshahin/workflows-sdk\";\nimport { SignedHttpAdapter } from \"@abshahin/workflows-sdk/http\";\n\nconst sendEmail = defineWorkflow(\"email/send\", {\n  cron: [\n    {\n      name: \"daily-digest\",\n      schedule: \"0 9 * * *\",\n      payload: { kind: \"daily-digest\" },\n    },\n  ],\n  retry: {\n    maxAttempts: 3,\n    initialIntervalMs: 1_000,\n    multiplier: 2,\n    maxIntervalMs: 30_000,\n  },\n  async run(ctx, payload) {\n    await ctx.step(\"send\", async () => {\n      console.log(\"send email\", payload);\n    });\n  },\n});\n\nexport const registry = defineWorkflowRegistry([sendEmail]);\n\nconst client = createWorkflowClient({\n  adapter: new SignedHttpAdapter({\n    baseUrl: \"https://workflows.example.com\",\n    authToken: process.env.WORKFLOWS_AUTH_TOKEN!,\n  }),\n});\n\nawait client.dispatch(\"email/send\", { tenantId: \"tenant_123\" });\n```\n\n### Dispatch Options\n\n`client.dispatch(name, payload, options)` accepts:\n\n| Option | Meaning |\n| --- | --- |\n| `id` | Explicit workflow instance ID |\n| `idempotencyKey` | Deduplication key used by adapters that support idempotency |\n| `delayMs` | Relative delay before the workflow should run |\n| `scheduledAt` | Absolute ISO string or `Date` for delayed execution |\n| `traceId` | Trace/correlation ID |\n| `metadata` | Extra envelope metadata |\n\nDelayed envelopes are stored as `scheduled` by Bun adapters. Cloudflare runner helpers sleep inside the Workflow before running user code.\n\n### Run Context\n\nEvery workflow receives a `ctx` object:\n\n| Method/property | Purpose |\n| --- | --- |\n| `ctx.step(name, fn, options?)` | Runs a durable/idempotent step when the adapter/runtime supports step storage |\n| `ctx.sleep(name, durationOrDate)` | Sleeps by duration string, milliseconds, or until a `Date` |\n| `ctx.dispatch(name, payload, options?)` | Dispatches another workflow through the configured client |\n| `ctx.event` | Original workflow envelope |\n| `ctx.traceId` | Trace ID from the envelope |\n| `ctx.idempotencyKey` | Idempotency key from the envelope |\n| `ctx.logger` | Runtime logger |\n\nStep results are cached by step name and workflow instance ID. Reusing the same step name for different side effects inside one workflow instance will reuse the first stored result.\n\n## Bun Runtime\n\nUse the Bun runtime when you want to execute workflows in a Bun process.\n\n```ts\nimport {\n  BunSqliteWorkflowAdapter,\n  createBunWorkflowRuntime,\n} from \"@abshahin/workflows-sdk/bun\";\nimport { registry } from \"./workflows\";\n\nconst runtime = createBunWorkflowRuntime({\n  registry,\n  adapter: new BunSqliteWorkflowAdapter({ path: \"workflows.sqlite\" }),\n  concurrency: 4,\n  scheduler: {\n    mode: \"external\",\n  },\n});\n\nawait runtime.client.dispatch(\"email/send\", { tenantId: \"tenant_123\" });\nawait runtime.tick();\nawait runtime.processReady();\n```\n\n### SQLite Adapter\n\n`BunSqliteWorkflowAdapter` is the recommended single-server adapter. It stores:\n\n- workflow instances\n- scheduled and queued state\n- idempotency keys\n- cron run claims\n- step results, including `undefined` results\n- dead letters\n\n```ts\nimport { BunSqliteWorkflowAdapter } from \"@abshahin/workflows-sdk/bun\";\n\nconst adapter = new BunSqliteWorkflowAdapter({\n  path: \"workflows.sqlite\",\n  namespace: \"production\",\n});\n```\n\n### Redis Adapter\n\n`BunRedisWorkflowAdapter` is intended for multi-instance Bun deployments. It uses Redis sorted sets, leases, idempotency keys, and step-result hashes.\n\n```ts\nimport { BunRedisWorkflowAdapter } from \"@abshahin/workflows-sdk/bun\";\n\nconst adapter = new BunRedisWorkflowAdapter({\n  url: Bun.env.REDIS_URL,\n  namespace: \"workflows\",\n  leaseTtlMs: 30_000,\n});\n```\n\nYou can also pass an existing Redis-like client:\n\n```ts\nconst adapter = new BunRedisWorkflowAdapter({\n  client: myRedisClient,\n});\n```\n\nThe adapter uses Bun's `RedisClient` when available. Raw Redis commands are sent through `redis.send(command, stringArgs)`.\n\n### Scheduler Modes\n\n| Mode | Use case | Behavior |\n| --- | --- | --- |\n| `external` | Kubernetes CronJob, systemd timer, queue worker, tests | You call `runtime.tick()` and/or `runtime.processReady()` yourself |\n| `in-process` | Long-running Bun process | Registers `Bun.cron(schedule, handler)` and processes due work in the same process |\n| `os` | Single-server production cron | Registers `Bun.cron(path, schedule, title)` and expects the target module to export `scheduled()` |\n| `redis` | Multi-instance Bun deployment | Uses Bun cron as a wake-up mechanism and Redis for claims/leases |\n\nFor OS-level Bun cron, register cron jobs from the long-running app:\n\n```ts\nconst runtime = createBunWorkflowRuntime({\n  registry,\n  adapter,\n  scheduler: {\n    mode: \"os\",\n    scriptPath: import.meta.path,\n    titlePrefix: \"my-app-workflows\",\n  },\n});\n\nruntime.start();\n```\n\nThe target module must export Bun's scheduled handler:\n\n```ts\nimport {\n  BunSqliteWorkflowAdapter,\n  createBunWorkflowScheduledHandler,\n} from \"@abshahin/workflows-sdk/bun\";\nimport { registry } from \"./workflows\";\n\nexport default createBunWorkflowScheduledHandler({\n  registry,\n  adapter: new BunSqliteWorkflowAdapter({ path: \"workflows.sqlite\" }),\n  scheduler: {\n    mode: \"os\",\n  },\n});\n```\n\nBun cron uses standard 5-field cron expressions. Bun parses and runs in-process cron schedules in UTC. OS-level Bun cron follows the host timezone because it delegates to the platform scheduler. The SDK accounts for that in `scheduled()` by evaluating OS cron ticks in the local timezone unless a cron definition sets `timezone`.\n\n### Cron Idempotency\n\nCron runs use deterministic keys:\n\n```txt\n${workflowName}:${cronName}:${scheduledAt.toISOString()}\n```\n\nSQLite or Redis stores the run key before dispatch, so duplicate wake-ups do not create duplicate workflow instances.\n\n`missedRunPolicy` defaults to `skip`. Use catch-up mode when you explicitly want multiple missed runs:\n\n```ts\nconst workflow = defineWorkflow(\"billing/hourly\", {\n  cron: [\n    {\n      name: \"hourly\",\n      schedule: \"0 * * * *\",\n      missedRunPolicy: { mode: \"catch-up-all\", maxRuns: 3 },\n    },\n  ],\n  run: async () => {},\n});\n```\n\n### Bun Retries and Recovery\n\nThe Bun runtime retries failed workflow runs when the workflow has a retry policy and the adapter implements `requeue()`.\n\n- Step-level retry happens inside `ctx.step()`.\n- Workflow-level retry requeues the same instance with attempt metadata.\n- If retries are exhausted, the instance is marked `dead` and recorded as a dead letter when the adapter supports it.\n- `recoverStalled()` returns stuck `running` instances to `queued` or `scheduled`.\n\n## Cloudflare Workflows\n\nThere are two Cloudflare integration paths.\n\n### Custom Worker Endpoint\n\nUse `SignedHttpAdapter` from any producer service that dispatches to your own Worker endpoint:\n\n```ts\nimport { createWorkflowClient } from \"@abshahin/workflows-sdk\";\nimport { SignedHttpAdapter } from \"@abshahin/workflows-sdk/http\";\n\nconst client = createWorkflowClient({\n  adapter: new SignedHttpAdapter({\n    baseUrl: \"https://workflows.worker.example.com\",\n    authToken: Bun.env.WORKFLOWS_AUTH_TOKEN!,\n  }),\n});\n\nawait client.dispatch(\"email/send\", { tenantId: \"tenant_123\" });\nawait client.getInstance(\"wf_123\", { name: \"email/send\" });\n```\n\nBoth HTTP adapters keep at most 1,000 process-local instance-ID/name hints for\nconvenient status lookups. Configure `instanceNameCacheSize` to change the LRU\nbound or set it to `0` to disable hints. For durable status tooling, always\npass `options.name`; cache eviction never affects explicit-name lookups.\n\nThe custom Worker endpoint must expose:\n\n| Endpoint | Purpose |\n| --- | --- |\n| `POST /dispatch` | Accepts `{ events: WorkflowEventEnvelope[] }` and creates Workflow instances |\n| `GET /status/:id?name=<eventName>` | Returns the instance status |\n| `GET /health` | Optional health check |\n\nYou can build that Worker with `createCloudflareDispatchHandler()`:\n\n```ts\nimport { createCloudflareDispatchHandler } from \"@abshahin/workflows-sdk/cloudflare\";\nimport { registry } from \"./workflows\";\n\nexport default createCloudflareDispatchHandler({\n  registry,\n  auth: {\n    bearerToken: (env: { AUTH_TOKEN: string }) => env.AUTH_TOKEN,\n  },\n  resolveWorkflow(eventName, env: { EMAIL_WORKFLOW: Workflow }) {\n    if (eventName.startsWith(\"email/\")) return env.EMAIL_WORKFLOW;\n    return null;\n  },\n});\n```\n\nThe handler calls the Cloudflare binding with explicit success/error retention for both `Workflow.create()` and `createBatch()`. The default is one day for successful instances and three days for errors; pass `retention` to override it. Scheduled envelopes are passed as params and delayed by the Workflow entrypoint helper.\n\nFor retry-safe deterministic IDs, provide `resolveReceiptStore` and the stable\nconcrete binding name through `resolveWorkflowIdentity`. The receipt store keys\nthat binding identity plus instance ID to a canonical SHA-256 envelope hash,\nuses a fenced five-minute creation lease, and persists an `ABSENCE_PROVEN` stage\nbefore the first create. Exact retries are deduplicated; a different envelope\nfor the same ID is rejected. Ambiguous creates and batch omissions are accepted\nonly when that durable absence proof preceded creation and status from the\nexplicitly resolved binding now proves the instance exists.\n\nReceipts become eligible for cleanup checks after 31 days by default, but this\nis not a deletion TTL. Cleanup deletes only when the stored workflow name maps\nto the same concrete binding identity and that binding explicitly reports the\ninstance missing. Existing or ambiguous instances are deferred for another\nbounded check. The default cleanup budget is two batches of six candidates;\nschedule the dedicated receipt cleanup cron every five minutes. Without a\ndurable receipt store, duplicates remain fail-closed.\n\n### Workflow Entrypoint Helper\n\nUse `createCloudflareWorkflowEntrypoint()` when you want Cloudflare Workflows to execute SDK workflow definitions directly.\n\n```ts\nimport { WorkflowEntrypoint } from \"cloudflare:workers\";\nimport { createCloudflareWorkflowEntrypoint } from \"@abshahin/workflows-sdk/cloudflare\";\nimport { registry } from \"./workflows\";\n\ninterface Env {\n  AUTH_TOKEN: string;\n}\n\nconst EmailWorkflowBase = createCloudflareWorkflowEntrypoint(\n  WorkflowEntrypoint<Env>,\n  { registry },\n);\n\nexport class EmailWorkflow extends EmailWorkflowBase {}\n```\n\nThe helper maps:\n\n- `ctx.step()` to `step.do()`\n- `ctx.sleep()` to `step.sleep()` or `step.sleepUntil()`\n- subsecond numeric sleeps to Cloudflare's millisecond duration API\n- `ctx.dispatch()` to a durable `step.do()` that creates the child Workflow\n- workflow retry/timeout options to Cloudflare step config\n- future `scheduledAt` envelopes to a durable Cloudflare sleep before user code runs\n\nGive every repeated child dispatch an explicit stable `childKey`. The runner derives a deterministic child instance ID and step name from the parent event plus that key, so replay cannot create another instance:\n\n```ts\nawait ctx.dispatch(\n  \"course/rebuild-part\",\n  { courseId, part: 1 },\n  { childKey: \"part-1\" },\n);\n```\n\nOne unnamed child per target workflow is allowed for compatibility; a second unnamed child to the same target is rejected as ambiguous. Workflow envelopes are validated as plain JSON and capped at 96 KB before any Cloudflare binding call.\n\n### Direct Cloudflare REST API\n\nUse `CloudflareRestWorkflowAdapter` when you want to dispatch directly to Cloudflare's public Workflows REST API instead of your own Worker endpoint.\n\n```ts\nimport { createWorkflowClient } from \"@abshahin/workflows-sdk\";\nimport { CloudflareRestWorkflowAdapter } from \"@abshahin/workflows-sdk/cloudflare\";\n\nconst client = createWorkflowClient({\n  adapter: new CloudflareRestWorkflowAdapter({\n    accountId: Bun.env.CLOUDFLARE_ACCOUNT_ID!,\n    apiToken: Bun.env.CLOUDFLARE_API_TOKEN!,\n    workflowName(eventName) {\n      if (eventName.startsWith(\"email/\")) return \"email-workflow\";\n      throw new Error(`No Cloudflare Workflow for ${eventName}`);\n    },\n  }),\n});\n\nawait client.dispatch(\"email/send\", { tenantId: \"tenant_123\" });\nawait client.getInstance(\"wf_123\", { name: \"email/send\" });\n```\n\nThe adapter posts:\n\n```json\n{\n  \"instance_id\": \"wf_123\",\n  \"params\": {\n    \"id\": \"wf_123\",\n    \"name\": \"email/send\",\n    \"payload\": {}\n  }\n}\n```\n\nto:\n\n```txt\n/accounts/{account_id}/workflows/{workflow_name}/instances\n```\n\nStatus lookup requires either a recent prior dispatch retained by the adapter's\nbounded cache or `getInstance(id, { name })`, because Cloudflare status\nendpoints are scoped to a workflow name.\n\n## Testing\n\nUse `InMemoryWorkflowAdapter` for unit tests:\n\n```ts\nimport { createWorkflowClient } from \"@abshahin/workflows-sdk\";\nimport { InMemoryWorkflowAdapter } from \"@abshahin/workflows-sdk/testing\";\n\nconst adapter = new InMemoryWorkflowAdapter();\nconst client = createWorkflowClient({ adapter });\n\nawait client.dispatch(\"email/send\", { tenantId: \"tenant_123\" });\n```\n\nFor runtime-level tests, use `BunSqliteWorkflowAdapter({ path: \":memory:\" })`.\n\n## Error Classes\n\nThe root export includes:\n\n- `WorkflowError`\n- `WorkflowSendError`\n- `WorkflowValidationError`\n- `WorkflowRetryExhaustedError`\n- `WorkflowNotFoundError`\n- `WorkflowAlreadyClaimedError`\n\n`SignedHttpAdapter` marks deterministic dispatcher failures (`400`, `401`,\n`403`, `404`, `409`, `413`, and `422`) as non-retryable by setting\n`error.nonRetryable = true`. Rotate the dispatch bearer token with an explicit\noverlap/deploy procedure; repeated billed dispatch retries cannot repair an\nexpired or mismatched credential.\n\n## Production Notes\n\n- Use Cloudflare Workflows for Worker deployments that need managed durable execution.\n- Use SQLite for one Bun process/server.\n- Use Redis for multiple Bun workers or multiple scheduler instances.\n- Keep workflow instance IDs under Cloudflare's current instance ID limit when using the REST adapter.\n- Keep workflow names under Cloudflare's current workflow name limit when using the REST adapter.\n- Use unique, stable step names. Step results are keyed by instance ID and step name.\n- Do not rely on Bun's fallback cron parser for production semantics. Production Bun scheduling should use Bun's native cron support.\n- In-process Bun cron uses UTC. OS-level Bun cron uses the host timezone.\n- `Bun.cron(path, schedule, title)` re-registers the same title in place, so keep `titlePrefix`, workflow name, and cron name stable.\n- This package currently ships TypeScript source via exports; compile before publishing to runtimes that cannot load TypeScript directly.\n\n## Verification\n\nUseful package-level checks:\n\n```bash\nbun test packages/workflows-sdk/src\nbunx tsc -p packages/workflows-sdk/tsconfig.json --noEmit\n```\n","readmeFilename":"README.md","bugs":{"url":"https://github.com/aashahin/workflows-sdk/issues"}}