Was this page helpful?
Was this page helpful?
They're most helpful when you have multiple tasks in a row, and you want to configure each task to be able to be retried if they fail.
If a task within a workflow fails, the Workflow will automatically "pick back up" on the task where it failed and not re-execute any prior tasks that have already been executed.
If you only need to run one operation, use a single Task. But if you need multiple steps that depend on each other, use a Workflow.
Example scenario: When a user signs up, you need to:
Without a workflow, if step 2 fails, you'd have to:
With a workflow:
The most important aspect of a Workflow is the handler, where you can declare when and how the tasks should run by simply calling the runTask function. If any task within the workflow, fails, the entire handler function will re-run.
However, importantly, tasks that have successfully been completed will simply re-return the cached and saved output without running again. The Workflow will pick back up where it failed and only task from the failure point onward will be re-executed.
To define a JS-based workflow, simply add a workflow to the jobs.workflows array in your Payload config. A workflow consists of the following fields:
Option | Description |
|---|---|
| Define a slug-based name for this workflow. This slug needs to be unique among both tasks and workflows. |
| The function that should be responsible for running the workflow. You can either pass a string-based path to the workflow function file, or workflow job function itself. If you are using large dependencies within your workflow, you might prefer to pass the string path because that will avoid bundling large dependencies in your Next.js app. Passing a string path is an advanced feature that may require a sophisticated build pipeline in order to work. |
| Define the input field schema - Payload will generate a type for this schema. |
| You can use interfaceName to change the name of the interface that is generated for this workflow. By default, this is "Workflow" + the capitalized workflow slug. |
| Define a human-friendly label for this workflow. |
| Optionally, define the queue name that this workflow should be tied to. Defaults to "default". |
| You can define |
| Control how jobs with the same concurrency key are handled. Jobs with the same key will run exclusively (one at a time). Requires |
Example:
In the above example, our workflow was executing tasks that we already had defined in our Payload config. But, you can also run tasks without predefining them.
To do this, you can use the inlineTask function.
The drawbacks of this approach are that tasks cannot be re-used across workflows as easily, and the task data stored in the job will not be typed. In the following example, the inline task data will be stored on the job under job.taskStatus.inline['2'] but completely untyped, as types for dynamic tasks like these cannot be generated beforehand.
Example:
One of the most powerful features of workflows is how they handle failures. Let's walk through what actually happens:
Example workflow:
First execution attempt:
createProfile) succeeds → Profile created in databasesendEmail) fails → Email service timeoutaddToList) never runs → Workflow pausesThe job is marked for retry. Task 2 has retries: 3, so it will be attempted again.
Second execution attempt (automatic retry):
Tasks can pass data to subsequent tasks through their outputs:
Task status structure:
When multiple jobs operate on the same resource, race conditions can occur. For example, if a user creates a document and then quickly updates it, two jobs might be queued that both try to process the same document simultaneously, leading to unexpected results.
The concurrency option allows you to prevent this by ensuring that jobs with the same "key" run exclusively (one at a time).
First, enable the feature in your Payload config:
Then add the concurrency option to your workflow configuration:
When you define a concurrency key:
processing: false and will be picked up on subsequent runsThe concurrency option accepts either a function (shorthand) or an object with more options:
Shorthand (function only):
Full configuration:
1. Exclusive only (preserve all jobs):
Use when every job represents unique work that must complete (e.g., processing distinct versions of a document).
2. Exclusive + Supersedes (last queued wins):
Use when only the latest state matters (e.g., regenerating embeddings after rapid edits - intermediate states can be skipped).
3. Queue-specific concurrency:
Include the queue name to allow the same resource to be processed concurrently in different queues.
When supersedes: true is set, newly queued jobs will automatically delete older pending (not yet running) jobs with the same concurrency key:
Example scenario:
Configuration:
When to use:
Important notes:
exclusive: true, supersedes still deletes pending jobs but won't prevent parallel executionsync:doc1 in the default queue will block a job with the same key in the emails queue. Include the queue name in your key if you want queue-specific concurrency.processing: false and will be picked up on subsequent runs.