Dry runs and history
Dry runs
$result = Workflows::definition('purchase-approval', $team)
->dryRun() // the current version, else the draft; dryRun(3) for v3
->subject($request) // or ->sample(['amount' => '60000', 'requester' => 5])
->input(['reason' => 'laptops'])
->event('purchase_request.submitted', ['amount' => '60000'])
->assume(['legal_cfo' => 'rejected']) // outcomes for steps that can't be simulated
->at(now()->addDay())
->by($user)
->log() // write one workflow_dry_runs row
->run(); // DryRunResult — or ->explore() for every branch (DryRunTree)
$result->nodes(); // the path's node ids
$result->path; // list<DryRunStep{node, type, outcome, assumed, at, input}>
$result->actions; // list<WouldBeAction{node, type, status, description, details}>
$result->endedBy; // end | limit | invalid | not_started
Workflows::versions()->dryRun($graphOrVersion, $definition)->sample([...])->run(); // an unsaved graphThe walker validates the graph, evaluates the trigger (input, trigger condition, owner match) and walks from the trigger with a virtual clock and step counter:
- pure types (condition, branch, loop, end) evaluate for real against the subject or sample;
- side-effect types call simulate() if they have one (its wouldDo() actions are listed); otherwise they are listed as would_run and take assume[node], else their first required outcome, marked assumed;
- delay computes its due time (business time included) and advances the clock;
- wait takes assume[node], else its first outcome; escalations are listed with their due times.
reason is one of invalid_graph, missing_trigger, owner_mismatch, invalid_input, condition_false, missing_edge. Each DryRunStep carries the input the step would see — its config with every reference resolved, rendered exactly like a real run’s step input.
It writes nothing. No handler runs, no model is saved, no job or event is dispatched, the passed subject is cloned, and everything happens inside a transaction that always rolls back. ->log() (or dry_run.log = on) writes one workflow_dry_runs row after the rollback — the only write. A sample accepts registered fields only.
Every branch
->explore() walks every way the graph can go: wherever an outcome had to be assumed (a side-effect step without simulate(), a wait), it branches into each outcome the step can end with. It is bounded by graph.max_nodes walks (a branch the budget can’t cover stays a leaf), never logs, and run() stays the single path:
$tree = Workflows::definition('purchase-approval', $team)->dryRun()->subject($request)->explore();
$tree->branchesAt; // 'approve'
array_keys($tree->branches); // ['approved', 'rejected', 'expired']
$tree->paths(); // list<DryRunResult>: every complete path, left to right
$tree->toArray(); // {result, branches_at, branches}History and the step log
Workflows::for($request)->instances(); // newest first, cursor-paginated
Workflows::definition('purchase-approval', $team)->instances(InstanceStatus::Waiting);
Workflows::instances()->find($ulidOrId);
Workflows::instances()->steps($instance); // every attempt, in order (each with its input)
Workflows::instances()->recent(InstanceStatus::Failed, 'purchase-approval', limit: 20);
Workflows::instances()->detail($instance); // loads the step log, pending waits and escalations
$instance->path(); // node ids of the completed steps
$instance->failure(); // FailureCode, a host step's string code, or nullEvery attempt records its input: the node config with each value reference resolved, models as {id, type}, capped by workflows.steps_input.max_bytes.
Run overlay
One grouped query counts the version’s running, waiting and failed instances per current node — the data a builder draws on top of the graph:
$overlay = Workflows::versions()->overlay($version); // OverlayReport{version, nodes, total}
$overlay->node('approve')?->waiting; // NodeOverlay{node, running, waiting, failed}Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.