Docs
Scheduling with QStash

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/step request
  • 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:

  1. Load conversation history from DB
  2. Get the current step's toolName and toolInput from workflowSteps
  3. Run ToolLoopAgent with the single step's tool, constrained by stopWhen: stepCountIs(1)
  4. saveAgentResponse() persists the result and assets
  5. If toolNeedsApproval: send step approval email, set run to awaiting_approval
  6. 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=9000

The 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.