---
title: "Dry runs and history — Workflows for Laravel | Roundly"
description: "Walk a workflow without writing anything — one path or every branch — then read instance history, the step log with resolved inputs and the run overlay."
url: https://roundly-consulting.com/open-source/docs/workflows-for-laravel/dry-runs-and-history
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Workflows for Laravel](https://roundly-consulting.com/open-source/docs/workflows-for-laravel.md)

# Dry runs and history

## Dry runs

```php
$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 graph
```

The 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:

```php
$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

```php
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 null
```

Every 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:

```php
$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 crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## 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.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/workflows-for-laravel.md)
