{"_id":"@auriclabs/jobs","_rev":"4-c3f858d5f141aaf387a78d04de50e4c8","name":"@auriclabs/jobs","dist-tags":{"latest":"0.4.0"},"versions":{"0.1.0":{"name":"@auriclabs/jobs","version":"0.1.0","keywords":[],"author":"","license":"ISC","_id":"@auriclabs/jobs@0.1.0","maintainers":[{"name":"kevupton","email":"kevin@upton.tech"}],"homepage":"https://github.com/auriclabs/packages#readme","bugs":{"url":"https://github.com/auriclabs/packages/issues"},"dist":{"shasum":"9f7b3572c6dd58509e723f22eaa20c5b614fccc1","tarball":"https://registry.npmjs.org/@auriclabs/jobs/-/jobs-0.1.0.tgz","fileCount":47,"integrity":"sha512-z1GWVV/cGupF+396jEMzAeRViDI5Q683wVM7kpCaIdDNDjAY42q/B2w6ZNCgD/0DOX7J5sFNxjchFQi3hPRJgQ==","signatures":[{"sig":"MEUCIQCdwhLHXHO+hjLc50SMSZv/ND2tMUcm+BcaOBkdMts3xQIgGxviuwTAYE1taU5L9oJAPS4T2q2rIYpw7AH/KgtrQes=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":252565},"main":"dist/index.cjs","type":"module","_from":"file:auriclabs-jobs-0.1.0.tgz","types":"dist/index.d.mts","module":"dist/index.mjs","exports":{".":{"types":"./dist/index.d.mts","import":"./dist/index.mjs","require":"./dist/index.cjs"}},"scripts":{"dev":"concurrently \"pnpm build --watch\" \"pnpm:y:watch\"","lint":"eslint .","test":"vitest run --passWithNoTests","build":"tsdown src/index.ts --format cjs,esm --dts --no-hash","y:watch":"chokidar dist --initial --silent -c \"yalc publish --push\"","lint:fix":"eslint . --fix","typecheck":"ts-config-typecheck","test:watch":"vitest"},"_npmUser":{"name":"kevupton","email":"kevin@upton.tech"},"prettier":"@auriclabs/prettier-config","_resolved":"/tmp/8f29495de63b3237aead9101a322ce61/auriclabs-jobs-0.1.0.tgz","_integrity":"sha512-z1GWVV/cGupF+396jEMzAeRViDI5Q683wVM7kpCaIdDNDjAY42q/B2w6ZNCgD/0DOX7J5sFNxjchFQi3hPRJgQ==","repository":{"url":"git+https://github.com/auriclabs/packages.git","type":"git","directory":"packages/jobs"},"_npmVersion":"10.8.2","description":"Job queue system with DynamoDB + SQS + Lambda execution","directories":{},"_nodeVersion":"20.20.1","dependencies":{"uuid":"^11.1.0"},"publishConfig":{"registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"@types/uuid":"^10.0.0","@auriclabs/logger":"0.1.0","@types/aws-lambda":"^8.10.148","@auriclabs/api-core":"0.1.0","@auriclabs/pagination":"1.0.0"},"peerDependencies":{"electrodb":"^3.0.0","@auriclabs/logger":"^0.1.0","@auriclabs/api-core":"^0.1.0","@aws-sdk/client-sqs":"^3.0.0","http-errors-enhanced":"^3.0.0","@auriclabs/pagination":"^0.1.0","@aws-sdk/client-lambda":"^3.0.0","@aws-sdk/util-dynamodb":"^3.0.0","@aws-sdk/client-dynamodb":"^3.0.0"},"_npmOperationalInternal":{"tmp":"tmp/jobs_0.1.0_1774134513799_0.2726721822154199","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@auriclabs/jobs","version":"0.2.0","keywords":[],"author":"","license":"ISC","_id":"@auriclabs/jobs@0.2.0","maintainers":[{"name":"kevupton","email":"kevin@upton.tech"}],"homepage":"https://github.com/auriclabs/packages#readme","bugs":{"url":"https://github.com/auriclabs/packages/issues"},"dist":{"shasum":"3484540e3be407d0eb5aa3c309177b338a775c28","tarball":"https://registry.npmjs.org/@auriclabs/jobs/-/jobs-0.2.0.tgz","fileCount":50,"integrity":"sha512-otxII2oP9nJUywullKiUlZda4jiGIFiu/bq67AAp82WOvTyhyn77bJ3zgWfliiOg2OPy+yU7TUNwZr3xMloEQg==","signatures":[{"sig":"MEQCIDCqxuTFluLV1cxrmwbTbPUGrIU3v0kD1xvnsje51QOUAiBff4xkqIq9sncTxXcHYgn6S5wVQ58YnyArnMeFPn1XZw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":254065},"main":"dist/index.cjs","type":"module","_from":"file:auriclabs-jobs-0.2.0.tgz","types":"dist/index.d.mts","module":"dist/index.mjs","exports":{".":{"types":"./dist/index.d.mts","import":"./dist/index.mjs","require":"./dist/index.cjs"}},"scripts":{"dev":"concurrently \"pnpm build --watch\" \"pnpm:y:watch\"","lint":"eslint .","test":"vitest run --passWithNoTests","build":"tsdown src/index.ts --format cjs,esm --dts --no-hash","y:watch":"chokidar dist --initial --silent -c \"yalc publish --push\"","lint:fix":"eslint . --fix","typecheck":"ts-config-typecheck","test:watch":"vitest"},"_npmUser":{"name":"kevupton","email":"kevin@upton.tech"},"prettier":"@auriclabs/prettier-config","_resolved":"/tmp/82bca3f363a0aff6919bffdc13a71603/auriclabs-jobs-0.2.0.tgz","_integrity":"sha512-otxII2oP9nJUywullKiUlZda4jiGIFiu/bq67AAp82WOvTyhyn77bJ3zgWfliiOg2OPy+yU7TUNwZr3xMloEQg==","repository":{"url":"git+https://github.com/auriclabs/packages.git","type":"git","directory":"packages/jobs"},"_npmVersion":"10.8.2","description":"Job queue system with DynamoDB + SQS + Lambda execution","directories":{},"_nodeVersion":"20.20.1","dependencies":{"uuid":"^11.1.0"},"publishConfig":{"registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"@types/uuid":"^10.0.0","@auriclabs/logger":"0.1.1","@types/aws-lambda":"^8.10.148","@auriclabs/api-core":"0.1.1","@auriclabs/pagination":"1.0.1"},"peerDependencies":{"electrodb":"^3.0.0","@auriclabs/logger":"^0.1.1","@auriclabs/api-core":"^0.1.1","@aws-sdk/client-sqs":"^3.0.0","http-errors-enhanced":"^3.0.0","@auriclabs/pagination":"^1.0.1","@aws-sdk/client-lambda":"^3.0.0","@aws-sdk/util-dynamodb":"^3.0.0","@aws-sdk/client-dynamodb":"^3.0.0"},"_npmOperationalInternal":{"tmp":"tmp/jobs_0.2.0_1774290597457_0.8303643028932006","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@auriclabs/jobs","version":"0.3.0","keywords":[],"author":"","license":"ISC","_id":"@auriclabs/jobs@0.3.0","maintainers":[{"name":"kevupton","email":"kevin@upton.tech"}],"homepage":"https://github.com/auriclabs/packages#readme","bugs":{"url":"https://github.com/auriclabs/packages/issues"},"bin":{"auric-jobs-dashboard":"dist/auric-jobs-dashboard.mjs"},"dist":{"shasum":"589234e9861e741f209b773ca8894f991ebe3e3f","tarball":"https://registry.npmjs.org/@auriclabs/jobs/-/jobs-0.3.0.tgz","fileCount":41,"integrity":"sha512-OdnRPmOVT5rGZmTUcjmX6EJJcCGst4sA5F4CmxV50fu0mlzGL7bJeagOcdSHTbrsaJkIt4pygKbjI4gzk/a8sA==","signatures":[{"sig":"MEYCIQDujQZ38tNUU5KDxVXzuCbCW/ZkVPAOGTwOi1SIiOCJ/QIhAMawxVi6l4jorqucXbrep8BY9SVDNpWN3gW++JXptKqB","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":906842},"main":"dist/index.cjs","type":"module","_from":"file:auriclabs-jobs-0.3.0.tgz","types":"dist/index.d.mts","module":"dist/index.mjs","exports":{".":{"types":"./dist/index.d.mts","import":"./dist/index.mjs","require":"./dist/index.cjs"},"./package.json":"./package.json"},"scripts":{"dev":"concurrently \"pnpm build --watch\" \"pnpm:y:watch\"","lint":"eslint .","test":"vitest run --passWithNoTests && pnpm test:ui","build":"tsdown src/index.ts --format cjs,esm --dts --no-hash && tsdown bin/auric-jobs-dashboard.ts --format esm --no-hash --no-clean && pnpm build:ui","test:ui":"cd ui && npx vitest run --passWithNoTests","y:watch":"chokidar dist --initial --silent -c \"yalc publish --push\"","build:ui":"cd ui && npx vite build","lint:fix":"eslint . --fix","typecheck":"ts-config-typecheck","test:watch":"vitest"},"_npmUser":{"name":"kevupton","email":"kevin@upton.tech"},"prettier":"@auriclabs/prettier-config","_resolved":"/tmp/d2f064bb819493f8538d6d1fbfff3a81/auriclabs-jobs-0.3.0.tgz","_integrity":"sha512-OdnRPmOVT5rGZmTUcjmX6EJJcCGst4sA5F4CmxV50fu0mlzGL7bJeagOcdSHTbrsaJkIt4pygKbjI4gzk/a8sA==","repository":{"url":"git+https://github.com/auriclabs/packages.git","type":"git","directory":"packages/jobs"},"_npmVersion":"10.8.2","description":"Job queue system with DynamoDB + SQS + Lambda execution","directories":{},"_nodeVersion":"20.20.2","dependencies":{"open":"^10.0.0","uuid":"^11.1.0","commander":"^13.1.0","@inquirer/prompts":"^7.0.0","@aws-sdk/credential-providers":"^3.700.0"},"publishConfig":{"registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"vite":"^7.3.2","react":"^19.0.0","react-dom":"^19.0.0","@types/uuid":"^10.0.0","tailwindcss":"4.2.4","@types/react":"^19.0.12","lucide-react":"1.12.0","@aws-sdk/types":"^3.700.0","@types/react-dom":"^19.0.4","react-router-dom":"^7.0.0","@auriclabs/logger":"0.1.2","@tailwindcss/vite":"4.2.4","@types/aws-lambda":"^8.10.148","@auriclabs/api-core":"0.1.2","@vitejs/plugin-react":"^5.2.0","@auriclabs/pagination":"1.0.1","@tanstack/react-query":"^5.0.0","@tanstack/react-table":"8.21.3","@radix-ui/react-dialog":"1.1.15"},"peerDependencies":{"electrodb":"^3.0.0","@auriclabs/logger":"^0.1.2","@auriclabs/api-core":"^0.1.2","@aws-sdk/client-sqs":"^3.0.0","http-errors-enhanced":"^3.0.0","@auriclabs/pagination":"^1.0.1","@aws-sdk/client-lambda":"^3.0.0","@aws-sdk/util-dynamodb":"^3.0.0","@aws-sdk/client-dynamodb":"^3.0.0"},"_npmOperationalInternal":{"tmp":"tmp/jobs_0.3.0_1783269180582_0.4003526678438536","host":"s3://npm-registry-packages-npm-production"}},"0.4.0":{"name":"@auriclabs/jobs","version":"0.4.0","description":"Job queue system with DynamoDB + SQS + Lambda execution","prettier":"@auriclabs/prettier-config","main":"dist/index.cjs","module":"dist/index.mjs","types":"dist/index.d.mts","exports":{".":{"types":"./dist/index.d.mts","import":"./dist/index.mjs","require":"./dist/index.cjs"},"./package.json":"./package.json"},"bin":{"auric-jobs-dashboard":"dist/auric-jobs-dashboard.mjs"},"keywords":[],"author":"","type":"module","license":"ISC","dependencies":{"@aws-sdk/credential-providers":"^3.700.0","@inquirer/prompts":"^7.0.0","commander":"^13.1.0","open":"^10.0.0","uuid":"^11.1.0"},"devDependencies":{"@aws-sdk/types":"^3.700.0","@radix-ui/react-dialog":"1.1.15","@tailwindcss/vite":"4.2.4","@tanstack/react-query":"^5.0.0","@tanstack/react-table":"8.21.3","@types/aws-lambda":"^8.10.148","@types/react":"^19.0.12","@types/react-dom":"^19.0.4","@types/uuid":"^10.0.0","@vitejs/plugin-react":"^5.2.0","lucide-react":"1.12.0","react":"^19.0.0","react-dom":"^19.0.0","react-router-dom":"^7.0.0","tailwindcss":"4.2.4","vite":"^7.3.2","@auriclabs/api-core":"0.1.2","@auriclabs/logger":"0.1.2","@auriclabs/pagination":"1.0.1"},"peerDependencies":{"@auriclabs/api-core":"^0.1.2","@auriclabs/logger":"^0.1.2","@auriclabs/pagination":"^1.0.1","@aws-sdk/client-dynamodb":"^3.0.0","@aws-sdk/client-lambda":"^3.0.0","@aws-sdk/client-sqs":"^3.0.0","@aws-sdk/util-dynamodb":"^3.0.0","electrodb":"^3.0.0","http-errors-enhanced":"^3.0.0"},"publishConfig":{"registry":"https://registry.npmjs.org/"},"repository":{"type":"git","url":"git+https://github.com/auriclabs/packages.git","directory":"packages/jobs"},"scripts":{"build":"tsdown src/index.ts --format cjs,esm --dts --no-hash && tsdown bin/auric-jobs-dashboard.ts --format esm --no-hash --no-clean && pnpm build:ui","build:ui":"cd ui && npx vite build","dev":"concurrently \"pnpm build --watch\" \"pnpm:y:watch\"","y:watch":"chokidar dist --initial --silent -c \"yalc publish --push\"","lint":"eslint .","lint:fix":"eslint . --fix","typecheck":"ts-config-typecheck","test":"vitest run --passWithNoTests && pnpm test:ui","test:ui":"cd ui && npx vitest run --passWithNoTests","test:watch":"vitest"},"_id":"@auriclabs/jobs@0.4.0","bugs":{"url":"https://github.com/auriclabs/packages/issues"},"homepage":"https://github.com/auriclabs/packages#readme","_integrity":"sha512-r1mj2N3/Yof5r5zJn67U8Br7H3mACqTQBqbf8nWHqSHO9p+V7rF50TgFlNlU6ZdNOvLz+434b0fwQ70c6doJ7Q==","_resolved":"/tmp/cdcb0ade47128f7a39d90cbff176b3d5/auriclabs-jobs-0.4.0.tgz","_from":"file:auriclabs-jobs-0.4.0.tgz","_nodeVersion":"20.20.2","_npmVersion":"10.8.2","dist":{"integrity":"sha512-r1mj2N3/Yof5r5zJn67U8Br7H3mACqTQBqbf8nWHqSHO9p+V7rF50TgFlNlU6ZdNOvLz+434b0fwQ70c6doJ7Q==","shasum":"5253b0aa7471b2b2065f596bd637460029e9434b","tarball":"https://registry.npmjs.org/@auriclabs/jobs/-/jobs-0.4.0.tgz","fileCount":42,"unpackedSize":930324,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIAryiN44hF8c6uzbO6OCstMSlTWUiRzso+pCLSyyqGgaAiAkRUBtjuC9p4Re8r0kQbkwdPS7/JVNXtGTps9KT2kb3g=="}]},"_npmUser":{"name":"kevupton","email":"kevin@upton.tech"},"directories":{},"maintainers":[{"name":"kevupton","email":"kevin@upton.tech"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/jobs_0.4.0_1783326564307_0.2700387751253026"},"_hasShrinkwrap":false}},"time":{"created":"2026-03-21T23:08:33.741Z","modified":"2026-07-06T08:29:24.709Z","0.1.0":"2026-03-21T23:08:33.950Z","0.2.0":"2026-03-23T18:29:57.627Z","0.3.0":"2026-07-05T16:33:00.761Z","0.4.0":"2026-07-06T08:29:24.581Z"},"bugs":{"url":"https://github.com/auriclabs/packages/issues"},"license":"ISC","homepage":"https://github.com/auriclabs/packages#readme","keywords":[],"repository":{"type":"git","url":"git+https://github.com/auriclabs/packages.git","directory":"packages/jobs"},"description":"Job queue system with DynamoDB + SQS + Lambda execution","maintainers":[{"name":"kevupton","email":"kevin@upton.tech"}],"readme":"# @auriclabs/jobs\n\nJob queue system built on DynamoDB + SQS + Lambda. Supports job scheduling, retry logic, FIFO ordering, and Lambda-based execution.\n\n## Setup\n\n```bash\npnpm add @auriclabs/jobs\n```\n\n### Peer dependencies\n\n```bash\npnpm add @auriclabs/api-core @auriclabs/logger @auriclabs/pagination\npnpm add @aws-sdk/client-dynamodb @aws-sdk/client-lambda @aws-sdk/client-sqs @aws-sdk/util-dynamodb\npnpm add electrodb http-errors-enhanced\n```\n\n## Initialization\n\nCall `initJobs()` once per Lambda cold start:\n\n```typescript\nimport { initJobs } from '@auriclabs/jobs';\nimport { Resource } from 'sst';\n\ninitJobs({ tableName: Resource.JobTable.name });\n```\n\nThis creates all ElectroDB entities and service instances. The job queue service reads `QUEUE_URL_LIST` from `process.env`, and the Lambda executor reads `LAMBDA_FUNCTION_LIST` from `process.env`.\n\n## API Reference\n\n### Scheduling Jobs\n\n```typescript\nimport { scheduleJob } from '@auriclabs/jobs';\n\nconst { job, jobAttempt } = await scheduleJob(\n  'lambda',                         // queue name\n  'MyWorkerFunction',               // Lambda function key\n  { url: 'https://example.com' },   // payload\n  '2025-12-01T00:00:00.000Z',      // optional: scheduled time (ISO string)\n);\n```\n\n### Job Lifecycle\n\nJobs follow this lifecycle:\n\n```\npending → running → completed\n                  → failed\n```\n\n1. `scheduleJob()` creates a job + first attempt in `pending` state\n2. DynamoDB stream triggers → job attempt added to SQS queue\n3. SQS triggers Lambda executor → `startJob()` marks as `running`\n4. Executor invokes target Lambda → `stopJob()` marks as `completed` or `failed`\n\n### Handlers\n\n#### `createJobTableStreamHandler()`\n\nCreates a DynamoDB Streams handler that watches for job attempt INSERTs and MODIFYs, adding them to the appropriate SQS queue.\n\n```typescript\nimport { createJobTableStreamHandler, initJobs } from '@auriclabs/jobs';\nimport { Resource } from 'sst';\n\ninitJobs({ tableName: Resource.JobTable.name });\n\nexport const handler = createJobTableStreamHandler();\n```\n\n#### `createLambdaExecutorHandler()`\n\nCreates an SQS handler that executes jobs by invoking Lambda functions. Supports both FIFO and standard queues.\n\n```typescript\nimport { createLambdaExecutorHandler, initJobs } from '@auriclabs/jobs';\nimport { Resource } from 'sst';\n\ninitJobs({ tableName: Resource.JobTable.name });\n\nexport const handler = createLambdaExecutorHandler();\n```\n\nFIFO queue behavior: processes sequentially, stops on first failure.\nStandard queue behavior: processes in parallel, collects individual failures.\n\n#### `createRegistryExecutorHandler(handlers)`\n\nIn-process alternative to the Lambda executor: instead of invoking a separate\ntarget Lambda per job `fn`, runs a registered handler inside the SQS consumer.\nNo second cold start, no cross-Lambda payload limit, one function to deploy.\n\n```typescript\nimport { createRegistryExecutorHandler, initJobs } from '@auriclabs/jobs';\nimport { Resource } from 'sst';\n\ninitJobs({ tableName: Resource.JobTable.name });\n\nexport const handler = createRegistryExecutorHandler({\n  syncItems: async (payload, context) => {\n    // return raw data — wrapped as { success: true, data }\n    return { processed: 42 };\n  },\n  sendEmail: async (payload) => {\n    // return nothing — wrapped as { success: true }\n  },\n});\n```\n\nHandlers return raw data (wrapped as a success response), a `JobContinuation`\n(see below), or throw to fail the attempt. An unregistered `fn` fails the\nattempt with a clear error. Wire the consumer with the `'in-process'` executor\nresource in `@auriclabs/jobs-infra`.\n\n### Typed job registry\n\n`defineJobs<T>()` adds compile-time safety over the untyped core: job `fn`\nkeys and payload shapes are checked when scheduling, and the handler map must\ncover every declared job type.\n\n```typescript\nimport { defineJobs } from '@auriclabs/jobs';\n\ninterface MyJobs {\n  syncItems: { cursor?: string };\n  cloneItem: { itemId: string; count: number };\n}\n\nexport const jobs = defineJobs<MyJobs>();\n\n// payload and fn key are type-checked\nawait jobs.scheduleJob('worker', 'cloneItem', { itemId: 'x', count: 2 });\n\n// missing or mistyped handlers are compile errors\nexport const handler = jobs.createRegistryExecutorHandler({\n  syncItems: async (payload) => { /* payload: { cursor?: string } */ },\n  cloneItem: async (payload) => ({ clonedId: payload.itemId }),\n});\n```\n\n### Long-running jobs (continuations)\n\nJobs that exceed the Lambda time limit can process a slice, then return a\ncontinuation carrying a cursor. The current attempt completes, the job goes\nstraight back to `pending` (never a false `completed` for status pollers),\nand a new attempt is created with the cursor in its `state` field.\n\n```typescript\nimport { continueJob, createTimeBudget } from '@auriclabs/jobs';\n\nexport const handler = jobs.createRegistryExecutorHandler({\n  syncItems: async (payload, context) => {\n    const budget = createTimeBudget(5 * 60 * 1000); // 5 minutes\n    let cursor = (context.jobAttempt.state as { cursor?: string } | undefined)?.cursor;\n\n    do {\n      cursor = await syncPage(cursor);\n    } while (budget.shouldRun(cursor));\n\n    if (cursor) {\n      return continueJob({ cursor }); // schedule the next slice\n    }\n    return { done: true };\n  },\n});\n```\n\nNote: each continuation slice counts as an attempt (`totalAttempts` includes\nslices, not just retries).\n\n### Job logs\n\nHandlers can capture log output onto the attempt row — visible in the\ndashboard's attempt detail, exactly like migration output:\n\n```typescript\nexport const handler = jobs.createRegistryExecutorHandler({\n  syncItems: async (payload, context) => {\n    context.log('starting sync', { cursor: payload.cursor });\n    context.logger.warn('rate limited, backing off');\n    // ...\n  },\n});\n```\n\nOutput is byte-capped (oldest lines drop first past ~200KB; the attempt row\nrecords `outputTruncated` when that happens) and persists on completion,\nfailure, and each continuation slice. Capture is per-attempt and\nconcurrency-safe — parallel jobs never interleave. Only in-process executors\ncan capture: jobs run through the Lambda executor log to the target Lambda's\nown CloudWatch instead.\n\n### Retrying failed jobs\n\n```typescript\nimport { retryJob } from '@auriclabs/jobs';\n\nconst { jobAttempt } = await retryJob('job-123');\n// or defer the retry:\nawait retryJob('job-123', '2025-12-01T00:00:00.000Z');\n```\n\nRetry is allowed from `pending`, `completed`, and `failed` — never while\n`running` or after `cancelled`. For long-running jobs, the retry carries the\nlast attempt's continuation `state`, so a failed slice resumes from its cursor\ninstead of restarting the whole job.\n\n### Helper Functions\n\n```typescript\nimport {\n  scheduleJob,    // Create job + first attempt\n  startJob,       // Validate and mark attempt as running\n  stopJob,        // Record completion/failure with duration\n  executeJob,     // Full lifecycle: start → execute → stop\n  getJobContext,  // Fetch job + attempt by ID\n} from '@auriclabs/jobs';\n```\n\n#### `executeJob(message, executor)`\n\nOrchestrates the full job lifecycle. Used internally by `createLambdaExecutorHandler()`, but can also be used for custom executors:\n\n```typescript\nimport { executeJob } from '@auriclabs/jobs';\n\nawait executeJob(\n  { jobId: 'job-123', queue: 'lambda', attempt: 1 },\n  async (context) => {\n    // context.job, context.jobAttempt available\n    const result = await doWork(context.job.payload);\n    return { success: true, data: result };\n  },\n);\n```\n\n### Services (via init getters)\n\nFor advanced use cases, access services directly:\n\n```typescript\nimport {\n  getJobService,\n  getJobAttemptService,\n  getJobQueueService,\n  getLambdaExecutorService,\n} from '@auriclabs/jobs';\n\nconst jobService = getJobService();\nconst job = await jobService.getJob('job-123');\nawait jobService.updateJob('job-123', { status: 'cancelled' });\n\nconst jobAttemptService = getJobAttemptService();\nconst attempts = await jobAttemptService.getAllJobAttempts('job-123');\n```\n\n## Types\n\n### `JobStatus`\n\n```typescript\nconst jobStatus = {\n  pending: 'pending',\n  running: 'running',\n  completed: 'completed',\n  failed: 'failed',\n  cancelled: 'cancelled',\n} as const;\n\ntype JobStatus = 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';\n```\n\n### `JobMessage`\n\n```typescript\ninterface JobMessage {\n  jobId: string;\n  queue: string;\n  attempt: number;\n}\n```\n\n### `JobResponse`\n\n```typescript\ninterface JobResponse {\n  success?: boolean;\n  error?: unknown;\n  data?: unknown;\n}\n```\n\n### `JobItem`\n\nElectroDB entity with fields: `id`, `queue`, `fn`, `status`, `totalAttempts`, `payload`, `createdAt`, `updatedAt`.\n\n### `JobAttemptItem`\n\nElectroDB entity with fields: `jobId`, `attempt`, `status`, `error`, `response`, `state`, `duration`, `startedAt`, `scheduledAt`, `completedAt`, `failedAt`, `createdAt`, `updatedAt`. `state` carries the continuation cursor for long-running jobs.\n\n### `JobExecutionError`\n\nCustom error class thrown when job execution fails. Has `.started` and `.completed` getters to determine job state.\n\n## Dashboard\n\nThe package ships a jobs dashboard (bundled Vite/React UI in `ui/` + a\nLambda API), modeled on the `@auriclabs/migrations` dashboard. It lists\njobs with status filtering, shows per-attempt history (errors, responses,\ncontinuation state, durations), and can retry or cancel jobs. It cannot\ncreate jobs.\n\n### Dashboard API handler\n\n```typescript\n// services/job/dashboard.ts\nimport { createJobsDashboardApiHandler, initJobs } from '@auriclabs/jobs';\nimport { Resource } from 'sst';\n\ninitJobs({ tableName: Resource.JobTable.name });\nexport const handler = createJobsDashboardApiHandler();\n```\n\nRoutes: `GET /api/jobs` (`status`/`cursor`/`limit` query params),\n`GET /api/jobs/summary`, `GET /api/jobs/:id` (job + attempts),\n`POST /api/jobs/:id/retry` (optional `{ scheduledAt }` body),\n`POST /api/jobs/:id/cancel` (pending jobs only, 409 otherwise).\n\nDeploy it with `createJobsDashboard()` from `@auriclabs/jobs-infra`.\nNote the API is CORS-`*` and unauthenticated (basic-auth only gates the\nstatic site) — deploy to dev/demo stages only.\n\n### Local CLI dashboard\n\n```bash\nauric-jobs-dashboard\n```\n\nPicks an AWS SSO profile (auto-runs `aws sso login` on expiry), discovers\nthe deployed job table by name, and serves the same UI on\n`http://127.0.0.1:3101` against the real table using your IAM identity.\n\n## Environment Variables\n\n| Variable | Format | Used by |\n|----------|--------|---------|\n| `QUEUE_URL_LIST` | `JSON.stringify([[\"queueId\", \"url\"], ...])` | `jobQueueService` |\n| `LAMBDA_FUNCTION_LIST` | `JSON.stringify([[\"fnKey\", \"arn\"], ...])` | `lambdaExecutorService` |\n\nThese are typically set by `@auriclabs/jobs-infra`'s `registerJobResources()`.\n\n## DynamoDB Table Schema\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `pk` | string | Partition key (job ID or job attempt composite) |\n| `sk` | string | Sort key |\n| `gsi1pk` / `gsi1sk` | string | GSI1 for job status queries |\n| `numberIndexPk` / `numberIndexSk` | string/number | Number index for job attempt ordering |\n\nUses ElectroDB for entity management with two entities (`job`, `job-attempt`) in the same table.\n","readmeFilename":"README.md"}