NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Workflows for Laravel

The Workflows facade

Everything goes through one facade, RoundlyConsulting\Workflows\Facades\Workflows (global alias Workflows), sugar over the WorkflowsManager container singleton. The manager holds no request state, so it is Octane-safe. Every mutating method has a non-null return type:

use RoundlyConsulting\Workflows\Facades\Workflows;

// Start and emit
Workflows::start('purchase-approval', $request, $team)->input([...])->by($user)->idempotencyKey($key)->dispatch();
Workflows::emit('purchase_request.submitted', $request, ['amount' => '1200'], idempotencyKey: "sub:{$request->id}", user: $user);
Workflows::for($request)->start('purchase-approval')->dispatch();   // the subject owner's definition, else the app-wide one

// Definitions and versions
Workflows::definitions()->create(key: 'purchase-approval', name: 'Purchase approval', subjectType: 'purchase_request', owner: $team);
Workflows::definition('purchase-approval', $team)->edit($graph);   // save the draft — see the definition handle
Workflows::definition('purchase-approval', $team)->publish(by: $admin);
Workflows::versions()->diff($v2, $v3);
Workflows::versions()->export($version);

// Runs
Workflows::instances()->find($ulid);
Workflows::instances()->retry($instance);
Workflows::instances()->cancel($instance, 'withdrawn');

// Registries a builder reads
Workflows::catalogue()->export();               // every step type with its config schema and outcomes
Workflows::subjects()->export('purchase_request');
Workflows::calendars()->names();
Workflows::triggers()->events();
Workflows::schema()->graph();                    // the graph JSON Schema

// The graph builder, wake and prune
Workflows::graph()->trigger('start', Trigger::manual())->end('done')->chain('start', 'done');
Workflows::wake();                               // what workflows:wake runs
Workflows::prune();                              // what workflows:prune runs

Manager methods

MethodReturnsDoes
start(string $key, ?Model $subject = null, ?Model $owner = null)PendingStartA manual start; ->dispatch() runs StartInstanceAction.
startWith(StartData $data)StartResultThe same from a DTO.
emit(string $event, ?Model $subject = null, array $payload = [], ?string $idempotencyKey = null, ?Model $user = null, ?Model $owner = null)EmitResultStarts every matching active definition and signals waiting steps; a definition that refuses to start is listed in failed.
emitWith(EmitData $data)EmitResultThe same from a DTO.
for(Model $subject)SubjectHandleStart, emit, history, cancel and touch for one subject.
definition(string $key, ?Model $owner = null)DefinitionHandleOne definition of one owner (null = app-wide).
definitions()DefinitionsAccessorcreate, createWith, find, exists, all, sync, restore.
versions()VersionsAccessorvalidate, diff, overlay, export, import, dryRun.
instances()InstancesAccessorfind, recent, detail, steps, retry, cancel.
catalogue()CatalogueAccessorregister, all, find, has, export — the step types.
subjects()SubjectsAccessorregister, find, export, all — the subject types.
subject(string $key, string $model)SubjectTypeBuilderDescribe a subject type to register.
calendars()CalendarsAccessorregister, find, names — business calendars.
triggers()TriggersAccessorwebhook, map, listen, events.
schema()SchemaAccessorgraph, template, definitions — the JSON Schemas.
graph()GraphBuilderThe fluent graph builder; pure, passes through the fake.
wake(?CarbonInterface $now = null)WakeReportOne wake pass (WakeAction); the whole pass runs at $now.
prune()PruneReportDeletes past-retention data (PruneAction).

signal(), perform() and make() are public on the manager but internal — the bridges’ and the handles’ plumbing. Don’t call them from app code.

Workflows::definition($key, $owner)

One definition of one owner (null = app-wide); another owner’s key is never found. Every method that needs the row throws UnknownDefinitionException (404) when it is missing:

$handle = Workflows::definition('purchase-approval', $team);

$handle->model();                         // WorkflowDefinition (UnknownDefinitionException if missing)
$handle->draft();                         // the draft version or null
$handle->edit($graph, $expectedChecksum); // save the draft (a graph array or a GraphBuilder; created as a copy of the current version)
$handle->discardDraft();
$handle->validate();                      // ValidationReport of the draft
$handle->publish(by: $admin);             // the draft becomes the current version
$handle->retire();                        // no new starts; running instances finish
$handle->activate();                      // allow starts again
$handle->resolveFlag($node, $path);       // dismiss a template review flag
$handle->rotateWebhook();                 // a new webhook id
$handle->versions();                      // every version, by number
$handle->version(3);
$handle->instances(InstanceStatus::Failed, perPage: 25);   // cursor-paginated history
$handle->start($subject);                 // PendingStart
$handle->dryRun(?int $version);           // PendingDryRun (NoVersionException for a missing version)
$handle->export(?int $version);           // TemplatePayload (NoVersionException for a missing version)
MethodReturnsDoes
model()WorkflowDefinitionThe row; UnknownDefinitionException (404) when missing.
draft()?WorkflowVersionThe draft or null.
edit(array|GraphBuilder $graph, ?string $expectedChecksum = null)WorkflowVersionSave the draft; a stale checksum throws DraftConflictException.
discardDraft()WorkflowDefinitionDrop the draft.
validate()ValidationReportFull validation of the draft.
publish(?Model $by = null)WorkflowVersionThe draft becomes the current version.
retire() / activate()WorkflowDefinitionStop or allow new starts; running instances finish.
resolveFlag(string $node, string $path)WorkflowVersionDismiss a template review flag.
rotateWebhook()WorkflowDefinitionA new webhook id.
versions() / version(int $number)Collection / ?WorkflowVersionEvery version by number, or one.
instances(?InstanceStatus $status = null, int $perPage = 25)CursorPaginatorHistory, newest first.
start(?Model $subject = null)PendingStartA manual start of this definition.
dryRun(?int $version = null)PendingDryRunThe current version, else the draft; NoVersionException for none or a missing number.
export(?int $version = null)TemplatePayloadA portable template; NoVersionException likewise.

Workflows::definitions()

MethodReturnsDoes
create(string $key, string $name, ?string $subjectType = null, ?Model $owner = null, ?string $description = null)WorkflowDefinitionA draft definition with version 1 holding the minimal graph (manual trigger → end).
createWith(CreateDefinitionData $data)WorkflowDefinitionThe same from a DTO.
find(string $key, ?Model $owner = null)?WorkflowDefinitionNull when missing.
exists(string $key, ?Model $owner = null)boolWhether the key exists in that scope.
all(?Model $owner = null)CollectionEvery definition of the owner.
sync(?string $path = null, ?Model $owner = null, bool $publish = false, bool $dryRun = false)SyncReportCreate, draft and publish from JSON files ($path null = workflows.sync.path).
restore(string $key, ?Model $owner = null)WorkflowDefinitionUn-trash a soft-deleted definition.

Workflows::versions()

MethodReturnsDoes
validate(array|GraphBuilder $graph, ?string $subjectType = null, ?Model $owner = null)ValidationReportValidate any graph.
diff(WorkflowVersion $from, WorkflowVersion $to)VersionDiffNode and edge changes between two versions of one definition.
overlay(WorkflowVersion $version)OverlayReportRunning, waiting and failed instances per current node.
export(WorkflowVersion $version)TemplatePayloadA roundly.workflows.template/v1 payload.
import(TemplatePayload|array $payload, ?Model $owner = null, ?string $key = null)ImportResultA new draft definition with remapped ids and review flags.
dryRun(WorkflowVersion|array|GraphBuilder $graph, ?WorkflowDefinition $definition = null)PendingDryRunA dry run of an unsaved graph or any version.

Workflows::instances()

MethodReturnsDoes
find(int|string $id)?WorkflowInstanceBy ULID (any case) or primary key; a value the key column can’t hold is a miss.
recent(?InstanceStatus $status = null, ?string $definition = null, ?Model $owner = null, int $limit = 25)CollectionNewest first.
detail(WorkflowInstance $instance)WorkflowInstanceLoads the definition, version, step log, pending waits and pending escalations.
steps(WorkflowInstance $instance)CollectionEvery attempt, in order.
retry(WorkflowInstance $instance)WorkflowInstanceRetry a failed instance from its failed step.
cancel(WorkflowInstance $instance, ?string $reason = null)WorkflowInstanceCancel a running or waiting instance.

Workflows::for($subject) and $subject->workflows()

MethodReturnsDoes
start(string $key, ?Model $owner = null)PendingStartThe subject owner’s definition, else the app-wide one; an explicit owner searches only that scope.
emit(string $event, array $payload = [], ?string $idempotencyKey = null, ?Model $user = null)EmitResultEmit about this subject.
instances(int $perPage = 25)CursorPaginatorNewest first, owner and starter eager-loaded.
running()CollectionRunning or waiting instances.
cancelAll(?string $reason = null)intInstances cancelled — children a cancelled parent took with it count; one that ended meanwhile is skipped.
touch()intCondition waits re-evaluated.

Other accessors

AccessorMethodReturns
catalogue()register(string|StepType $type)StepType
catalogue()all() / find($key) / has($key)array / ?StepType / bool
catalogue()export()CatalogueExport
subjects()register(SubjectTypeBuilder|SubjectTypeDefinition $type)SubjectTypeDefinition
subjects()find($key) / export($key) / all()?SubjectTypeDefinition / SubjectTypeDefinition / array
calendars()register(string $name, Closure $resolver)self
calendars()find(string $name, ?Model $owner = null, ?string $timezone = null)BusinessCalendar
calendars()names()list<string>
triggers()webhook(string $hookId, array $payload, array $headers = [])StartResult
triggers()map(string $event, string $key, ?string $subject, ?string $user, list|string|null $payload, ?string $idempotencyKey)void
triggers()listen(string $event) / events()void / array<string, string>
schema()graph() / template() / definitions()array

Builders

BuilderMethods
PendingStartinput(array $input), by(?Model $user), idempotencyKey(?string $key), withForm(Request $request, bool $submit = false) → self; dispatch(): StartResult
PendingDryRunsubject(Model), sample(array), input(array), event(string $key, array $payload = []), assume(array), at(CarbonInterface), log(bool = true), by(?Model) → self; run(): DryRunResult, explore(): DryRunTree
SubjectTypeBuilderlabel(string), ownedBy(string $relation), ownerType(bool = true), ability(string), field(string $name, FieldType|string $type, ?string $label = null, string|array|null $options = null, ?string $path = null, bool $writable = false) → self; key(), build()

Value objects, DTOs, models and enums

  • DTOs (all final readonly, public promoted properties): StartData, StartResult (wasSkipped(), wasRefused()), EmitData, EmitResult (started, resumed, failed; skipped()), EmitFailure (definition, exception; code()), CreateDefinitionData, DryRunData, DryRunResult, DryRunStep, DryRunTree, WouldBeAction, SideEffect, SimulatedStep, ValidationReport, ValidationError, EdgeReference, VersionDiff, NodeChange, EdgeChange, OverlayReport, NodeOverlay, TemplatePayload, ImportResult, ReviewFlag, SyncReport, SyncedDefinition, WakeReport, PruneReport, CatalogueExport, CatalogueEntry, SubjectTypeDefinition, FieldDefinition, EventMapping, PartyReference, Duration, RetryPolicy, Actor, Signal, StepAuthorization, TransitionOutcome, DoctorFinding.
  • Graph\Graph, Graph\Node and Graph\Edge are the parsed, read-only graph (StepContext::graph(), node(), WorkflowVersion::parsedGraph()): node($id), nodesOfType($type), trigger(), outgoing($id), incoming($id), edgeFor($from, $outcome), setting($key, $default), toArray().
  • Models (swappable via workflows.models.*) have $guarded = [], a factory, a configurable key and UTC timestamps. All soft-delete except WorkflowIdempotencyKey; workflows:prune force-deletes.
  • Enums are string-backed with translated labels through enums-for-laravel’s helpers.
ModelRelationsMethods
WorkflowDefinitionowner() MorphTo, versions() HasMany, currentVersion() BelongsTo, instances() HasManyisActive()
WorkflowVersiondefinition() BelongsTo, publisher() MorphToreviewFlags(), setReviewFlags(array), isPublished(), parsedGraph(): Graph
WorkflowInstancedefinition(), version(), parent() BelongsTo; owner(), subject(), starter() MorphTo; children(), stepRuns(), waits(), escalations() HasManyfailure(): FailureCode|string|null, path(): list<string>
StepRuninstance() BelongsTo, waits(), escalations() HasManyerror(): FailureCode|string|null
WorkflowWaitinstance(), stepRun() BelongsTo—
WorkflowEscalationinstance(), stepRun() BelongsTo—
WorkflowIdempotencyKeyinstance() BelongsTo—
WorkflowDryRundefinition() BelongsTo, requester() MorphTo—
EnumCases
ActorModesystem, on_behalf_of
ConditionOperatorequals, in, gt, lt, between, empty, contains, before, after
DefinitionStatusdraft, active, retired
DoctorLevelok, warn, fail
DurationUnitminutes, hours, days, weeks, months, business_minutes, business_hours, business_days; isBusiness()
EscalationStatuspending, claimed, fired, failed, cancelled
FieldTypestring, text, integer, decimal, boolean, date, datetime, enum, party, list
InstanceStatusrunning, waiting, completed, failed, cancelled; isTerminal(), isActive()
Severityerror, warning
SignalKindtimer, event, condition, probe, retry, timeout
StepRunStatusqueued, running, waiting, completed, failed, cancelled
SyncStatuscreated, drafted, published, unchanged, invalid, failed
TriggerTypemanual, event, schedule, webhook
VersionStatusdraft, published, superseded
WaitKindtimer, event, condition, retry, timeout
WaitStatuspending, claimed, fired, cancelled
ValidationCodeEvery publish code — see Publish validation.
FailureCodeEvery engine, built-in and bridge failure code; fromStored(), detail(), retryable(), message() — see Troubleshooting.
WorkflowOperationWhat the fake records — see Testing.

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

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