| name | async-job-pattern |
| description | Pattern for async long-running tasks with GenerationJob polling in Takdi. |
Trigger
- Use when adding long-running API operations (image generation, video render, etc.).
- Use when implementing fire-and-forget background processing with status polling.
Read First
src/app/api/projects/[id]/generate-images/route.ts — canonical implementation
prisma/schema.prisma — GenerationJob model
src/lib/api-response.ts — response helpers
Pattern
POST Handler (Start Job)
const job = await prisma.$transaction(async (tx) => {
const newJob = await tx.generationJob.create({
data: {
projectId: id,
status: "queued",
provider: "provider-name",
input: JSON.stringify({ }),
},
});
await tx.usageLedger.create({
data: {
workspaceId,
eventType: "event_type_start",
detail: JSON.stringify({ projectId: id, jobId: newJob.id }),
},
});
return newJob;
});
processInBackground(job.id, ).catch(console.error);
return jsonOk({ jobId: job.id, status: "queued" }, 202);
Background Processing Function
async function processInBackground(jobId: string, ) {
try {
await prisma.generationJob.update({
where: { id: jobId },
data: { status: "running", startedAt: new Date() },
});
const result = await doExpensiveWork();
await prisma.generationJob.update({
where: { id: jobId },
data: {
status: "done",
output: JSON.stringify(result),
doneAt: new Date(),
},
});
} catch (error) {
await prisma.generationJob.update({
where: { id: jobId },
data: { status: "failed", error: String(error), doneAt: new Date() },
});
}
}
GET Handler (Poll Status)
return jsonOk({
job: { id, status, provider, error, startedAt, doneAt },
...(status === "done" ? { assets } : {}),
});
Job Status Lifecycle
queued → running → done
→ failed
Key Rules
- Always create job in a transaction with UsageLedger
- Always use fire-and-forget with
.catch(console.error) — never await in request handler
- Always update job status at each transition (queued → running → done/failed)
- Always capture error string in job.error on failure
- Return 202 (Accepted), not 200, for async operations
- Process items sequentially in background to respect rate limits
Validation
- POST returns 202 with jobId immediately
- GET returns current job status with appropriate data
- Failed jobs have error field populated
- UsageLedger has start event recorded