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

Publish validation

$report = Workflows::definition('purchase-approval', $team)->validate();          // the draft
$report = Workflows::versions()->validate($graph, 'purchase_request', $team);     // any graph
$report->valid();          // no errors (warnings don't block)
$report->errors;           // list<ValidationError>
$report->errorsOnly();     // and ->warnings()
$report->find(ValidationCode::UnknownField, 'check');
$report->has(ValidationCode::UnknownField, 'check');
$report->toArray();

ValidationError{code: ValidationCode, severity: error|warning, message (translated), node: ?string, edge: ?{from, to, outcome}, path: ?string (JSON pointer), params: array}. Messages never contain a class name. publish() runs the full validation under the definition lock and throws InvalidGraphException (carrying the report) on errors; its message lists the first problems. Saving a draft checks structure only:

use RoundlyConsulting\Workflows\Exceptions\InvalidGraphException;

try {
    Workflows::definition('purchase-approval', $team)->publish(by: $admin);
} catch (InvalidGraphException $e) {
    $e->validationReport()->toArray();   // [{code, severity, message, node, edge, path, params}, …]
}

Passes

All passes run and collect errors; only a structural failure stops early:

  • Structure: schema version, node/edge shape, id pattern, duplicate ids, dangling edges, size caps.
  • Catalogue: known node types; a bridged type whose package is missing → adapter_missing with params.package.
  • Config: the type’s JSON schema (invalid_config + path), then its semantic checks, then escalation blocks.
  • Trigger/end: exactly one trigger, at least one end, no edge into the trigger, none out of an end.
  • Outcomes: each required outcome has exactly one edge; no unknown or duplicate outcomes.
  • Reachability: every node is reachable from the trigger.
  • Cycles: with the guarded edges removed — a loop’s continue edge and the way out of a delay with a positive for — no cycle remains.
  • Fields and refs: field paths, operators, value types, writability, inputs, ancestor refs, the subject’s owner resolution.
  • Actor: on_behalf_of needs a trigger with a user.
  • Import flags: unresolved template review flags.
  • Cross-definition: start_workflow targets exist for the owner, are manual, and get their required inputs; a static self-start without wait: false is a warning.

Codes

CodeMeaning
invalid_schema_versionschema is not roundly.workflows.graph/v1
invalid_structureThe document’s shape is wrong
invalid_node_idAn id breaks ^[a-z][a-z0-9_]{0,63}$ (the message never repeats the submitted id)
duplicate_nodeTwo nodes share an id
dangling_edgeAn edge points at an unknown node
graph_too_largeOver a size cap
unknown_node_typeNo step type with that key
adapter_missingThe type needs a package that isn’t installed
invalid_configThe config breaks the type’s schema or rules
trigger_countNot exactly one trigger
no_endNo end node
trigger_has_incomingAn edge leads into the trigger
end_has_outgoingAn edge leaves an end
missing_outcomeA required outcome has no edge
unknown_outcomeAn edge names an outcome the type doesn’t have
duplicate_outcomeTwo edges for one (from, outcome)
outcome_label_requiredAn unlabelled edge from a node whose outcomes are not just next (params: outcome, the type’s outcomes)
unreachable_nodeA node can’t be reached from the trigger
cycle_without_breakerA cycle passes through no loop continue edge and no positive for delay (params.nodes)
unknown_fieldA field path isn’t registered
operator_not_allowedThe operator doesn’t suit the field type
invalid_valueA literal of the wrong type
unknown_inputinput.* isn’t declared
invalid_referencesteps.<id> isn’t an ancestor
field_not_writableset_field on a non-writable or lifecycle-state field
subject_owner_unresolvableAn owned definition over a subject type without owner resolution
unknown_subject_typeThe definition’s subject type isn’t registered
actor_requires_useron_behalf_of with a schedule or webhook trigger
unresolved_import_referenceA template review flag is still open
unknown_definitionA start_workflow target doesn’t exist for the owner
child_trigger_not_manualA start_workflow target isn’t manual
missing_child_inputA required child input isn’t mapped
self_startWarning: a definition starts itself
unknown_calendarA calendar name isn’t known for the owner
roles_unavailableA role party without a roles resolver
invalid_cronA cron expression the parser refuses (params.token)
condition_too_complexToo deep or too many clauses
unknown_transitionLifecycle bridge: no such transition
unknown_notification_typeNotifications bridge: no such type
unknown_formA form trigger names a form forms-for-laravel doesn’t have (without forms installed: adapter_missing)

Input from the browser is untrusted: validation and draft saves answer any document — wrong types at every level, deep nesting, huge strings — with a ValidationReport or InvalidGraphException, never a TypeError, and no error message echoes a submitted value.

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.