Handle async job queues in n8n workflows. Polling patterns and webhook-based completion handling.
This document describes two production-ready patterns for integrating n8n with Workflow's queued API (/api/ask, /api/edit).
For API details (payloads, jobId, /api/jobs/:jobId), see docs/API_OPERATIONS.md.
When you call POST /api/ask or POST /api/edit, Workflow always queues the request and returns immediately:
{ queued: true, jobId: string, ... }Jobs are retrieved by polling:
GET /api/jobs/:jobId → returns { status: pending|processing|completed|failed, result?, error? }Key point: All requests return a jobId that must be polled for results.
POST /api/ask or POST /api/editjobId immediatelyGET /api/jobs/{{ $json.jobId }}completed: go to Process Resultfailed: go to Handle FailureThis is the preferred design for large Claude requests and higher volume.
You split responsibilities:
Store a tracking record per submitted job. Minimum schema:
'mcp' | 'manual' (see “Use cases” below)ask|editqueued|processing|completed|failedStorage options:
POST /api/ask or POST /api/edit)
jobId immediatelyjobId + metadataTrigger:
Steps:
queued, processing)GET /api/jobs/:jobIdpending/processing: update timestamps/attempts and continuefailed: mark failed + propagate error to taskcompleted: mark completed + call Result Processor/api/jobs/:jobId should retry.processing for too long, mark “timed out” on your side and alert.Polling is simple, but it’s not the only option. For large requests, a completion webhook is often more efficient:
callback objectWebhooks can fail due to networking, restarts, n8n downtime, TLS issues, etc. Recommended:
jobId once)When callback.secret is provided, Workflow includes:
x-workflow-signature: sha256=<hex> (HMAC-SHA256 of the JSON body)The JSON body:
{
"jobId": "…",
"type": "claude-code",
"status": "completed|failed",
"timestamp": "…",
"result": {
/* present when completed */
}
}
{
"workspaceId": "your-workspace-id",
"question": "…",
"callback": {
"url": "https://n8n.example.com/webhook/your-hook",
"secret": "shared-secret"
}
}
Use this if you set callback.secret:
import crypto from 'node:crypto';
function timingSafeEqual(a, b) {
const ba = Buffer.from(a);
const bb = Buffer.from(b);
if (ba.length !== bb.length) return false;
return crypto.timingSafeEqual(ba, bb);
}
export function verifyWorkflowSignature({ secret, rawBody, signatureHeader }) {
if (!signatureHeader?.startsWith('sha256=')) return false;
const provided = signatureHeader;
const digest = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const expected = `sha256=${digest}`;
return timingSafeEqual(provided, expected);
}
Goal: Claude updates the Linear issue using MCP tools, so your automation doesn’t need to parse the model output to update the task.
Recommended approach:
mode = "mcp".jobId and optionally comments “Running…”.result.output and/or an error.Why still poll?
What is the “return” for MCP mode?
job.result.output.docs/PROMPT_TEMPLATES.md).job.result.output and use either:
callback.url), orGET /api/jobs/:jobId) as a fallback.Goal: you want a predictable payload that n8n can parse and write back to the task.
Recommended approach:
mode = "manual".job.result.output and parses JSON. If parsing fails, fall back to “raw text” update.Suggested JSON contract:
{
"status": "completed|blocked|needs_review",
"summary": "1-3 sentence summary",
"changes": {
"highLevel": ["..."],
"notes": ["..."]
},
"artifacts": {
"branch": "feature/...",
"commitHash": "abc123...",
"mergeRequestUrl": null
},
"nextSteps": ["..."]
}
Notes:
/api/edit, you can also use job.result.postExecution (commit hash / pushed branch / MR URL) as the source of truth for artifacts.contractVersion: 1).jobId + metadata/api/jobs/:jobId with retry/backoffprocessedAt timestamp or processed = true