Create a workflow
Use the serve
method to define an endpoint that serves a workflow. It provides a context object that you use to create all of your workflow’s steps.
For example, in a Next.js app:
import { serve } from "@upstash/qstash/nextjs"
export const POST = serve(async (context) => {
const result = await context.run("step-1", async () => {
// define a piece of business logic as step 1
})
await context.run("step-2", async () => {
// define another piece of business logic as step 2
})
})
Platform support
Besides Next.js, the serve
method supports multiple frameworks and platforms. See all supported platforms here.
Options
failureUrl
Use the failureUrl
option to specify a URL your workflow will call if it exhausted all retries and fails.
export const POST = serve<string>(
async (context) => { ... },
{
failureUrl: "https://<YOUR-FAILURE-ENDPOINT>/..."
}
);
If specified, this URL will be called with the failure callback payload, and the error message will be included in the body
field.
The default value is undefined
, meaning no failure URL is called.
failureFunction
Use the failureFunction
to define a function that’s executed when your workflow exhausts all its retries and fails.
export const POST = serve<string>(
async (context) => { ... },
{
failureFunction: async (
context, // context during failure
failStatus, // failure status
failResponse, // failure message
failHeaders // failure headers
) => {
// handle the failure
}
}
);
If both failureUrl
and failureFunction
are provided, the failure function takes precedence and the value of failureUrl
is ignored.
Note that if you are using a custom authorization method, you should also add authentication to failureFunction
.
By default, failureFunction
is undefined
, meaning that no function is executed on failure.
retries
To specify the number of times QStash will call the workflow endpoint in case of errors, you can use the verbose
parameter.
The default value is 3.
export const POST = serve<string>(
async (context) => { ... },
{
retries: 3
}
);
verbose
To gain insights into how the workflow operates, you can enable verbose mode:
export const POST = serve<string>(
async (context) => { ... },
{
verbose: true
}
);
Each log entry has the following structure:
{
timestamp: number,
workflowRunId: string,
logLevel: string,
eventType: string,
details: unknown,
}
The eventType
can be:
ENDPOINT_START
each time the workflow endpoint is calledRUN_SINGLE
orRUN_PARALLEL
when step(s) are executedSUBMIT_STEP
when a single step is executedSUBMIT_FIRST_INVOCATION
when a new workflow run startsSUBMIT_CLEANUP
when a workflow run finishesSUBMIT_THIRD_PARTY_RESULT
when a third-party call result is received (seecontext.call
)
Verbose mode is disabled by default.
initialPayloadParser
When calling the workflow endpoint to start a workflow run, your initial request’s payload is expected to be either empty, a string, or JSON.
If your payload differs, you can process it as needed using the initialPayloadParser
option:
type InitialPayload = {
foo: string
bar: number
}
// 👇 1: provide initial payload type
export const POST = serve<InitialPayload>(
async (context) => {
// 👇 3: parsing result is available as requestPayload
const payload: InitialPayload = context.requestPayload
},
{
// 👇 2: custom parsing for initial payload
initialPayloadParser: (initialPayload) => {
const payload: InitialPayload =
parsePayload(initialPayload)
return payload
},
}
)
url
Since a workflow run involves multiple calls to the workflow endpoint, the serve
method needs to know where your endpoint is hosted.
Typically, your workflow infers this using the request.url
field.
However, if you use a proxy or a local tunnel for development, you may want to override the URL inferred from request.url
:
export const POST = serve<string>(
async (context) => { ... },
{
url: "https://<YOUR-DEPLOYED-APP>.com/api/workflow"
}
);
By default, url
is undefined
, and the URL is inferred from request.url
.
baseUrl
An alternative to the url
option is the baseUrl
option. While url
replaces the entire URL inferred from request.url
, baseUrl
only changes the base of the URL:
export const POST = serve<string>(
async (context) => {
...
},
// options:
{
baseUrl: "<LOCAL-TUNNEL-PUBLIC-URL>"
}
);
The default value of baseUrl
is undefined
.
If you have multiple endpoints and you don’t want to set baseUrl
in every single one, you can set the UPSTASH_WORKFLOW_URL
environment variable to apply baseUrl
across your entire application.
Setting this environment variable is especially useful during local development. In production, baseUrl
or UPSTASH_WORKFLOW_URL
are not necessary.
Further options
The following options can be considered convenience methods. They are intended to support edge cases or testing pipelines and are not required for regular use.
qstashClient
The qstashClient
option allows you to pass a QStash Client explicitly. This can be helpful when using multiple QStash clients in the same project with different environment variables.
import { Client } from "@upstash/qstash";
import { serve } from "@upstash/qstash/nextjs";
export const POST = serve(
async (context) => { ... },
{
qstashClient: new Client({ token: process.env.QSTASH_TOKEN })
}
);
By default, a qstashClient
is initialized as:
new Client({
baseUrl: process.env.QSTASH_URL!,
token: process.env.QSTASH_TOKEN!,
})
receiver
You can pass a QStash Receiver to verify that every request the endpoint receives comes from QStash, preventing anyone from triggering your workflow.
import { Receiver } from "@upstash/qstash";
import { serve } from "@upstash/qstash/nextjs";
export const POST = serve<string>(
async (context) => { ... },
{
receiver: new Receiver({
// 👇 grab these variables from your QStash dashboard
currentSigningKey: process.env.QSTASH_CURRENT_SIGNING_KEY!,
nextSigningKey: process.env.QSTASH_NEXT_SIGNING_KEY!,
})
}
);
The default receiver is automatically used if the environment variables QSTASH_CURRENT_SIGNING_KEY
and QSTASH_NEXT_SIGNING_KEY
are set. If you want to turn off the Receiver, remove these environment variables or pass receiver: undefined
in the options. Note that this will skip any verification that requests are coming from QStash and allow anyone to start your workflow.
env
By default, the SDK uses process.env
to initialize the QStash client and the receiver (if the two environment variables are set). If process.env
doesn’t exist, the SDK won’t be able to access the environment variables. In this case, you can either pass qstashClient
and receiver
options or use the env
option.
If you pass env
, this env
will be used instead of process.env
:
import { Receiver } from "@upstash/qstash";
import { serve } from "@upstash/qstash/nextjs";
export const POST = serve<string>(
async (context) => {
// the env option will be available in the env field of the context:
const env = context.env;
},
{
receiver: new Receiver({
// 👇 grab these variables from your QStash dashboard
currentSigningKey: process.env.QSTASH_CURRENT_SIGNING_KEY!,
nextSigningKey: process.env.QSTASH_NEXT_SIGNING_KEY!,
})
}
);
Was this page helpful?