Skip to main content

Publishing & versions

Every workflow has an editable draft and a history of immutable versions. You edit the draft freely; nothing you change affects running workflows until you publish it. Publishing snapshots the draft as the next version number and makes that version the one that runs.

What it is
DraftThe Workflow row itself. workflow:build and your own code edit it.
VersionA numbered, read-only snapshot (v1, v2, …) of the draft's name, trigger, nodes, edges, and meta.
Published versionThe one version that runs. A workflow has at most one.

Only published workflows run​

Workflow::run(), Workflow::dispatch(), webhooks, schedules, model events, and Call workflow nodes all run the published version. A workflow that has never been published does not run — Workflow::run() throws a RuntimeException (Workflow [send-welcome-email] has not been published.), and its trigger is not wired up at boot.

Editing the draft after publishing changes nothing until you publish again.

Publishing​

php artisan workflow:publish send-welcome-email --note="Add a follow-up email"

Or in code:

use Qanna\WorkflowEngine\Models\Workflow;

$workflow = Workflow::findOrFail('send-welcome-email');

$version = $workflow->publish('Add a follow-up email');

$version->version; // 3
  • The first publish creates v1; each later publish creates the next number.
  • If the draft is identical to the published version, nothing is created and the current published version is returned.
  • Save the model before publishing. Calling publish() on a model with unsaved changes to its content throws a LogicException.
  • Toggling active is not a content change and never needs a publish.
$workflow->isPublished(); // has any version been published?
$workflow->hasUnpublishedChanges(); // does the draft differ from the published version?
$workflow->publishedVersion; // ?WorkflowVersion
$workflow->versions; // every version, as WorkflowVersion models

Triggers that need boot-time wiring (webhook, schedule, model event) pick up a newly published version on the next application boot — restart your app and queue workers after publishing.

Rolling back​

A rollback copies an earlier version into a new version number and publishes that. Later versions stay in the history, and the draft is left untouched.

php artisan workflow:rollback send-welcome-email 2 --note="Revert follow-up email"
$version = $workflow->rollbackTo(2, 'Revert follow-up email');
// With v4 published, this publishes v5 with v2's content.
  • The default note is Rollback to v2.
  • Rolling back to content that is already published creates nothing.
  • rollbackTo() with a version number that doesn't exist throws Illuminate\Database\Eloquent\ModelNotFoundException.

Running the draft​

To try a draft without publishing it:

php artisan workflow:run send-welcome-email --draft
$execution = Workflow::runDraft('send-welcome-email', ['email' => 'ada@example.com']);

A draft run works for workflows that have never been published. Its execution records workflow_version as null; a run of a published version records the version number.

Exporting and importing​

Export workflows to JSON files to keep them in git or move them between environments, then import them on the other side.

php artisan workflow:export # published version of every workflow
php artisan workflow:export send-welcome-email # one workflow
php artisan workflow:export --all # every version
php artisan workflow:export --latest=3 # the newest 3 versions
php artisan workflow:export --draft # also the draft, if it has unpublished changes
php artisan workflow:export --path=database/workflows

php artisan workflow:import # every workflow found in the directory
php artisan workflow:import send-welcome-email
php artisan workflow:import --path=database/workflows
php artisan workflow:import --force # replace drafts that have unpublished changes

Both default to resources/workflows.

File layout​

Each workflow gets its own directory:

resources/workflows/
└── send-welcome-email/
├── workflow.json # { "id", "active", "published_version" }
├── v1.json
├── v2.json
└── v3.draft.json # the draft, exported as the version after the latest one
  • Without --draft, a workflow that has never been published is skipped with a warning.
  • With --draft, a draft that matches the published version is not written, and any draft file left from an earlier export is removed.

Sensitive values​

Fields declared ->sensitive() (such as the Webhook trigger's secret) are never written in plain text. Exported files contain the placeholder {__SENSITIVE__} instead.

On import, a placeholder is filled from the value already stored in that environment — from the same version if it exists, otherwise from the draft — so importing never overwrites a secret you've set. When there is no stored value (for example, a workflow imported into a fresh database), the field is imported as empty.

Warning: a Webhook trigger with an empty secret accepts requests without an X-Webhook-Secret header. After importing a webhook workflow into a new environment, set its secret before relying on it.

What import does​

  • New workflows are created with the exported versions, the published version from workflow.json, and the draft file (or the published version's content when there is no draft file).
  • Existing versions are never changed. If v2 already exists with different content, it is skipped with a warning.
  • The published version only moves forward. An export that publishes an older version than the one currently published keeps the current one, with a warning.
  • Drafts with unpublished changes are kept, with a warning, unless you pass --force.
  • Version notes and creation dates are preserved.

Each workflow is imported in a single database transaction.