orlyapps/nova-workflow
orlyapps/nova-workflow brings configurable workflows to Laravel Nova resources. Define states and transitions, show current status, and move models through approval or publishing steps using Nova actions, with transition buttons, guards, and activity history for clearer admin-side process control.
composer.json rather than relying on a floating constraint.composer require orlyapps/nova-workflow
php artisan vendor:publish --provider="Orlyapps\\NovaWorkflow\\NovaWorkflowServiceProvider"
You should end up with a workflow config file (typically config/workflow.php) where places, transitions, and the initial place are declared per workflow name.status or state column). Add that column via migration, make it fillable, and cast it if needed. The model also needs the accessor path the package uses to read/write the marking — verify this against the config rather than assuming.use Orlyapps\NovaWorkflow\Fields\Workflow;
public function fields(Request $request)
{
return [
Workflow::make('Status', 'status')
->workflow('post_approval'),
// ... other fields
];
}
draft → review → published state machine on a Post model. Render the field on the detail view first — the buttons map cleanly to allowed transitions, and you get immediate feedback on whether the marking store, guards, and permissions are wired correctly. Only move to index/list usage once detail works.The first place to look when something is off is the generated config file, not the package source — 90% of "the button does nothing" reports trace back to a transition name mismatch or a marking-store attribute that isn't persisting.
Start with state machines, graduate to workflows. If your domain has a single linear lifecycle (draft → review → published → archived), define it as a state machine. Reserve multi-place workflows (a subject occupying several places simultaneously, e.g. "content approved" and "legal signed off") for genuine parallel-approval cases. Over-modelling early makes the Nova UI unreadable because every legal transition becomes a button.
Centralise transition side effects in event listeners, not in the config. Subscribe to the workflow's transition/entered events and do the work there:
Event::listen('workflow.post_approval.transition', function ($event) {
$event->getSubject()->recordAudit($event->getTransition());
});
This keeps the config declarative and makes side effects testable and queued independently.
Gate buttons by role/permission. Combine the field's visibility/authorization with the workflow's guard configuration so that a user who can't perform a transition doesn't see a dead button. Two layers: the field-level canSee/canRun on the Nova field (UI), and guards in the workflow definition (enforcement). Never rely on the Nova field alone — transitions can also be triggered programmatically.
Treat the Nova field as a thin control panel, not the source of truth. Put business rules in guards (can($subject, $transition)) and assert them again in the code path that actually performs the transition. The Nova button calls apply() under the hood; anything that can call apply() (an artisan command, a queued job, an API endpoint) must hit the same guards.
Audit trail pattern. Add a workflow_transitions table and write a row in a listener for every applied transition: subject, from-place, to-place, transition name, actor, timestamp. This is the single highest-value addition — it turns a fragile "the status is wrong" conversation into a query. It also lets you build a Nova Lens or a read-only HasMany field showing history on the detail page.
Multi-workflow models. When a model has two independent lifecycles (e.g. Order has both payment and fulfilment), give each a distinct workflow name and a distinct backing column. Render two separate Workflow fields, clearly labelled. Do not let both workflows share a single status column.
Testing transitions. Drive the unit tests through the package's API rather than through Nova HTTP tests:
$this->assertTrue($order->workflow_can('ship'));
$order->workflow_apply('ship');
$this->assertTrue($order->workflow_can('refund'));
Write one test per guard: allowed-from state, disallowed-from state, and role restriction. These tests catch 95% of regressions when you edit the config.
composer why-not orlyapps/nova-workflow and read the package's own composer.json to see what Symfony Workflow version it needs. Budget for a fork if you're on current Nova — the field is small enough (a Vue component plus a PHP field class) that forking or vendoring is a reasonable fallback.php artisan config:clear (and re-cache in deploy). A stale cached config produces "transition does not exist" errors that look like typos but aren't.$fillable, is it being saved (the field may save via a separate request that bypasses your normal form save), and does the accessor path in config match the real column name exactly? A status vs state mismatch is the classic failure.expression/guard string syntax for anything that needs to survive config:cache.FormRequest lifecycle. If the transition requires data (a rejection reason, an approver note), either use a Nova Action with a modal instead of the inline button, or enforce the requirement in a guard that reads the already-saved request data.Workflow fields on one workflow. Rendering the same workflow twice on a detail page produces duplicate buttons and confusing double-apply behaviour. Use one field per workflow name.Workflow field to add icons, colours, or confirmation modals.apply() when you need a confirmation form or bulk transitions.HasMany history field on the detail page for visibility.Workflow::make(...)->workflow('x') exactly the config key? (2) does php artisan tinker show $model->workflow_can('transition') returning true? (3) does the button appear in the Network tab with a POST to the field's endpoint, and what does the response say? If step 2 fails, the problem is config/guards. If step 2 passes but the button fails, the problem is the Nova field or authorization.Log::info('workflow', [...]) inside a listener will save you an afternoon the first time a guard silently rejects a transition.How can I help you explore Laravel packages today?