Scheduling with QStash
How to schedule workflows on a cron, and how the durable step execution model works.
Why QStash
QStash is a durable message queue by Upstash. Vercel serverless functions time out (60s on Hobby, 300s on Pro). A workflow with multiple LLM calls per step can easily exceed this. QStash solves this by splitting execution across multiple invocations:
- Each step is an independent
POST /api/workflows/steprequest - QStash handles retries, deduplication, and scheduling
- No single invocation ever runs more than one step
Creating a Scheduled Workflow
Pass a cron string in schedule when creating the workflow:
POST /api/workflows
{
"goal": "Publish a weekly LinkedIn post about AI trends",
"schedule": "0 9 * * 1" // every Monday at 9am UTC
}The cron string is stored in workflows.schedule. The QStash schedule ID is stored in workflows.qstashScheduleId once the schedule is registered.
Standard cron format: minute hour day-of-month month day-of-week
Step Execution Flow
POST /api/workflows/step is the QStash consumer endpoint, protected by verifySignatureAppRouter:
// Payload from QStash
{
workflowId: string,
runId: number,
conversationId: string,
stepIndex: number,
userResponse?: string, // set when resuming after approval
}For each invocation:
- Load conversation history from DB
- Get the current step's
toolNameandtoolInputfromworkflowSteps - Run
ToolLoopAgentwith the single step's tool, constrained bystopWhen: stepCountIs(1) saveAgentResponse()persists the result and assets- If
toolNeedsApproval: send step approval email, set run toawaiting_approval - Else: enqueue next step or mark run
completed
Deduplication
Every qstash.publishJSON() call includes a deterministic deduplication ID:
deduplicationId: `wf-${workflowId}-run-${runId}-step-${stepIndex}`
QStash will not re-enqueue a message with the same dedup ID within its deduplication window. This prevents double-execution when the consumer retries a failed step.
Local Development
Create a free Upstash account and get your QStash credentials from the console. For local development, Upstash provides a local QStash server that mirrors the production API. See the QStash local development guide for full details.
NPX (Node Package Executable)
Install and run the binary via the @upstash/qstash-cli package:
npx @upstash/qstash-cli dev
# Start on a different port
npx @upstash/qstash-cli dev -port=8081
# Start with a custom log server port
npx @upstash/qstash-cli dev -port=8081 -log-port=9000The local server starts on port 8080 by default and exposes the same REST API as production QStash. Set QSTASH_URL to http://localhost:8080 in your .env and use any non-empty string for the signing keys — the local server accepts them as-is.
Never disable verifySignatureAppRouter in production. It ensures only legitimate QStash requests trigger step execution. The local dev server generates valid signatures automatically, so no code changes are required between dev and prod environments.