- Overview
- Step types
- Context (
ctx) emit(value)andgetStepData(stepName, opts?)progress(data)- Lifecycle
- Concurrency
- Limits
- Notes
- See also
Overview
Scenarios split long-running scripts into managed steps. Each step runs independently, avoiding execution timeouts when processing large datasets.
A scenario script defines a sequence of steps using step(), loop(), and batch() functions. When you run the script, the system:
- Analyzes the script to extract all step definitions
- Creates a scenario run with step records in the database
- Executes steps sequentially
- Passes results from completed steps to subsequent steps via
ctx
Step types
step(name, fn)
A simple one-time step. The callback receives ctx - an object with results from all completed previous steps.
step('count', (ctx) => {
return Issues.count('project = PROJ')
})
step('report', (ctx) => {
log(`Total: ${ctx.count}`)
})
| Parameter | Type | Description |
|---|---|---|
name |
string | Unique step name (used as key in ctx) |
fn |
function | (ctx) => result - step callback |
The return value is stored as ctx[name] for subsequent steps.
loop(name, fn, initialState)
A step that can re-execute multiple times. Use next(state) to continue the loop or return a value to finish.
loop('paginate', (state, ctx) => {
var result = Issues.search('project = PROJ', {
maxResults: 100,
nextPageToken: state.cursor,
})
var keys = state.keys.concat(result.map(i => i.key))
if (result.hasMore) {
return next({ cursor: result.nextPageToken, keys })
}
return { allKeys: keys }
}, { cursor: null, keys: [] })
| Parameter | Type | Description |
|---|---|---|
name |
string | Unique step name |
fn |
function | (state, ctx) => result - loop callback |
initialState |
any | Initial state for the first iteration |
- Call
next(newState)to schedule another iteration with updated state - Return any other value to complete the step (stored in
ctx[name]) - Each iteration runs independently
batch(name, config, fn)
Processes issues in pages with automatic pagination, error handling, and progress tracking.
Config can be a JQL string (shorthand) or a config object with source, fields, and options.
// JQL string shorthand:
batch('cleanup', 'project = PROJ AND updated < -180d', (issue, ctx) => {
issue.transition('Done')
})
// Config object with fields and options:
batch('export', {
jql: 'project = PROJ',
fields: ['summary', 'status'],
expand: ['changelog'],
batchSize: 50,
}, (issue, ctx) => {
log(issue.key + ': ' + issue.summary)
})
| Parameter | Type | Description |
|---|---|---|
name |
string | Unique step name |
config |
string / object | JQL string or config object (see below) |
fn |
function | (issue, ctx) => void - called for each issue |
Config object properties
| Property | Type | Default | Description |
|---|---|---|---|
jql |
string | - | JQL query (one of jql/keys/source required) |
keys |
string[] | - | Array of issue keys |
source |
function | - | (ctx) => keys[] - dynamic source from previous steps |
fields |
string[] | all | Fields to load per issue |
expand |
string[] | none | Expand options (e.g. ["changelog"]) |
batchSize |
number | 100 | Issues per page (max 100) |
maxIssues |
number | unlimited | Stop after processing this many issues |
continueOnError |
boolean | true | Continue processing if an issue callback fails |
Batch result
The batch step stores its result in ctx[name]:
{
processed: 4523, // successfully processed issues
failed: 12, // failed issues
errors: ["PROJ-5: transition failed", ...], // first 100 error messages
errorsCapped: true, // true when there are more errors than shown
totalPages: 46 // number of batch iterations
}
Context (ctx)
Each step callback receives ctx - an object containing results from all previously completed steps.
step('find', () => {
return { total: Issues.count('project = PROJ') }
})
step('report', (ctx) => {
// ctx.find.total is available here
log(`Found ${ctx.find.total} issues`)
})
- Keys are step names, values are return values from those steps
- Only completed steps appear in
ctx - The context is serialized to JSON between steps (max 20MB)
emit(value) and getStepData(stepName, opts?)
Transfer data between steps without storing it in ctx. Useful when a step processes many items and you need to pass individual results to the next step.
emit(value) saves a value to the step’s data buffer. Call it multiple times inside step(), loop(), or batch() callbacks.
getStepData(stepName, opts?) reads emitted data from a previous step with pagination. Returns { items, total }.
batch('collect', 'project = PROJ', (issue, ctx) => {
emit({ key: issue.key, summary: issue.summary })
})
step('report', (ctx) => {
var page = getStepData('collect', { offset: 0, limit: 100 })
// page = { items: [{key:'PROJ-1', summary:'...'}, ...], total: 1500 }
log('Total collected: ' + page.total)
})
emit(value) Parameters
| Parameter | Type | Description |
|---|---|---|
value |
any | JSON-serializable value to save |
getStepData(stepName, opts?) Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
stepName |
string | required | Name of the step that emitted data |
opts.offset |
number | 0 | Number of items to skip |
opts.limit |
number | 1,000 | Max items to return (max 10,000) |
When to use emit() vs return
- Use
returnwhen the step result is small (configuration, counts, a short list of keys) - Use
emit()when the step generates many individual items (issue data, processing results) and you need paginated access
progress(data)
Report custom progress data from within a step callback. The data is stored and visible in the UI during execution.
step('process', (ctx) => {
var keys = ctx.find.keys
for (var i = 0; i < keys.length; i++) {
Issues.get(keys[i]).addLabel('processed')
progress({ current: i + 1, total: keys.length })
}
})
For batch() steps, progress is reported automatically (processed/failed counts).
Lifecycle
- Compile - script is compiled and analyzed for step definitions
- Create - a scenario run is created with status
running - Execute - steps are executed sequentially via Forge Queue events
- Complete - run status becomes
completedwhen last step finishes - Failed - if any step fails, run status becomes
failed
Cancellation
Running scenarios can be cancelled. After cancellation:
- The current step finishes (cancellation is checked between steps)
- Remaining steps are marked as
skipped - Run status becomes
cancelled
Retry
Failed and completed scenarios can be retried from any step:
- Steps from the retry point onward are reset to
pending - The scenario resumes using the original script version stored with the run
- Previously completed steps before the retry point keep their results
Stale recovery
If a step stays in running status for more than 10 minutes (e.g., due to a Forge timeout or OOM), the system automatically resets it to pending on the next queue delivery attempt.
Concurrency
Only one scenario run per script can be active at a time. Starting a new scenario while one is already running returns an error. Cancel the running scenario first or wait for it to complete.
Limits
See Limits for all scenario limits including step count, result sizes, batch sizes, emit limits, and API call budgets.
Notes
async/awaitin scenario callbacks is optional - API calls are auto-awaited- Step names must be unique within a scenario
- Steps are always executed in declaration order
- Each step runs with a fresh API call budget
next()can only be used insideloop(), not instep()- Scripts must be saved before running as a scenario
See also
- Scripting API - Full API reference
- Use Cases - Practical examples including bulk scenarios
- Async Events - Lower-level async execution
- Limits - All system limits
