Skip to main content

Parallel execution

Reqon runs independent actions in parallel when you group them in a pipeline stage.

Basic parallel execution

run [ActionA, ActionB, ActionC] then MergeResults

Execution:

┌─ ActionA ─┐
├─ ActionB ─┼─ MergeResults
└─ ActionC ─┘

The three actions run concurrently. Once all of them finish, MergeResults runs.

Syntax

Parallel group

run [Action1, Action2, Action3]

All actions in the bracket run together.

Parallel then sequential

run [Fetch1, Fetch2] then Process then [Export1, Export2]
┌─ Fetch1 ─┐ ┌─ Export1 ─┐
│ ├─ Process ────┤ │
└─ Fetch2 ─┘ └─ Export2 ─┘

Concurrency bound

Parallel stages run with a built-in limit of 8 actions in flight. If a bracket holds more than 8 actions, the rest start as earlier ones finish. There's no DSL option to change this bound.

Conditional stages

Any stage can carry an if condition. The stage runs only when the condition evaluates truthy, otherwise it's skipped:

run FetchData then ProcessData if env.shouldProcess then Export

Conditions work on parallel stages too:

run [SyncA, SyncB] if env.fullSync then Reconcile

Use cases

Fetching from multiple sources

mission MultiSourceSync {
source Xero { auth: oauth2, base: "https://api.xero.com" }
source QuickBooks { auth: oauth2, base: "https://quickbooks.api.com" }
source Stripe { auth: bearer, base: "https://api.stripe.com" }

store xeroInvoices: memory("xero")
store qbInvoices: memory("qb")
store stripeInvoices: memory("stripe")

action FetchXero {
get "/invoices" { source: Xero }
store response -> xeroInvoices { key: .InvoiceID }
}

action FetchQuickBooks {
get "/invoices" { source: QuickBooks }
store response -> qbInvoices { key: .Id }
}

action FetchStripe {
get "/invoices" { source: Stripe }
store response -> stripeInvoices { key: .id }
}

action Reconcile {
// All invoices are now available across the shared stores
for xero in xeroInvoices {
// Cross-reference with the other sources
}
}

run [FetchXero, FetchQuickBooks, FetchStripe] then Reconcile
}

Fan-out, fan-in

mission FanOutFanIn {
source API { auth: bearer, base: "https://api.example.com" }
store items: memory("items")
store pricing: memory("pricing")
store inventory: memory("inventory")

action FetchMaster {
get "/items" { source: API }
store response -> items { key: .id }
}

action EnrichWithPricing {
for item in items {
get concat("/pricing/", item.id) { source: API }
store response -> pricing { key: item.id }
}
}

action EnrichWithInventory {
for item in items {
get concat("/inventory/", item.id) { source: API }
store response -> inventory { key: item.id }
}
}

run FetchMaster then [EnrichWithPricing, EnrichWithInventory]
}

Failure semantics

Parallel stages are complete-then-fail:

  • Every branch runs to completion. There's no cancellation of siblings.
  • After all branches finish, the stage fails if any branch failed.
  • There's no rollback. A branch that wrote to a store keeps those writes even when a sibling fails.
run [ActionA, ActionB, ActionC]
// If ActionB fails, ActionA and ActionC still run to completion,
// then the stage reports the failure.

Shared state

Isolated action scope

Each branch gets its own action scope, so the step counter, checkpoints, and response are independent:

action ParallelA {
get "/a" { source: API } // its own response
}

action ParallelB {
get "/b" { source: API } // its own response, doesn't see A's
}

Shared stores

Stores, sources, and schemas are shared across branches. Writes to the same key are last-writer-wins, so parallel branches should target disjoint keys:

action ParallelA {
get "/a" { source: API }
store response -> shared { key: concat("a-", .id) }
}

action ParallelB {
get "/b" { source: API }
store response -> shared { key: concat("b-", .id) }
}

Best practices

// Good: related fetches that have no ordering dependency
run [FetchOrders, FetchOrderItems, FetchOrderPayments] then ProcessOrders

Use disjoint store keys

// Good: per-source key prefix avoids collisions
store response -> shared { key: concat(source, "-", .id) }

// Risky: branches may overwrite each other
store response -> shared { key: .id }

Troubleshooting

Actions not running in parallel

Check the bracket syntax:

run [A, B, C] // parallel
run A then B then C // sequential

Last-writer-wins surprises

If parallel branches write the same key, only the last write survives. Give each branch a distinct key prefix.