Advanced Host Integration
Headless runtime:
dbflowlabs/coredoes not ship Filament pages, business guards, or assignee business rules. This page lists what your Laravel application must wire aftercomposer require dbflowlabs/core.
Use this checklist before calling DBFlow::start() in production. Skipping a step is the most common cause of WorkflowNotFoundException, empty assignee lists, or approvals that never appear in UI.
Integration flow
Install Core → publish config/migrations → migrate
→ attach HasWorkflow to models
→ register WorkflowDefinitionProvider (host provider `booted()`)
→ register AssigneeResolver keys (when using callback / permission assignees)
→ php artisan dbflow:sync (deploy / CI)
→ expose start + inbox + business guards in your app
→ optional WorkflowHooks for host status columns
1. Install and configure
composer require dbflowlabs/core
php artisan vendor:publish --tag=dbflow-config
php artisan vendor:publish --tag=dbflow-migrations
php artisan migrate
Use ^1.0 constraints in production (for example dbflowlabs/core:^1.0). See Installation for Packagist and path-repository options.
config/dbflow.php
| Key | Default | Purpose |
|---|---|---|
enabled |
true |
Feature flag. When false, runtime APIs (start, approve, reject, cancel, reassign) throw WorkflowNotAvailableException; dbflow:sync and dbflow:validate remain available. |
binding_mode |
code |
code = explicit start / startWorkflow(). ui = auto-start on model created when dbflow_workflows.model_type matches. |
auth.model |
from env | Host user model FQCN (DBFLOW_AUTH_MODEL). |
auth.guard |
from env | Guard for DbflowAuth::currentUser() (DBFLOW_AUTH_GUARD). |
auth.resolver |
ConfigUserResolver |
Override when you need custom actor lookup (UUID/ULID stacks). |
visual_builder_enabled |
false |
Pro / visual builder flag. |
Example .env:
DBFLOW_ENABLED=true
DBFLOW_BINDING_MODE=code
DBFLOW_AUTH_MODEL=App\Models\User
DBFLOW_AUTH_GUARD=web
2. Register providers after the application boots
Create a host service provider (recommended) instead of inline registration in routes or controllers.
DBFlow Core binds WorkflowDefinitionRegistry and related services during its own boot() method. If your host provider calls DBFlow::registerDefinitionProvider() too early in register() or at the start of boot(), the registry singleton may not exist yet and php artisan dbflow:sync will report no providers.
Use $this->app->booted() so registration runs after all service providers have booted:
<?php
namespace App\Providers;
use App\Support\Dbflow\AssigneeResolvers\FinanceTeamAssigneeResolver;
use App\Support\Dbflow\Definitions\RefundDisputeWorkflowProvider;
use App\Support\Dbflow\RefundDisputeWorkflowHooks;
use DbflowLabs\Core\DBFlow;
use DbflowLabs\Core\Services\AssigneeResolverRegistry;
use DbflowLabs\Core\Services\WorkflowDefinitionRegistry;
use DbflowLabs\Core\Services\WorkflowHooksRegistry;
use Illuminate\Support\ServiceProvider;
final class AppDbflowServiceProvider extends ServiceProvider
{
public function boot(): void
{
$this->app->booted(function (): void {
if (! config('dbflow.enabled', true)) {
return;
}
DBFlow::registerDefinitionProvider(
$this->app->make(WorkflowDefinitionRegistry::class),
$this->app->make(RefundDisputeWorkflowProvider::class),
);
DBFlow::registerAssigneeResolver(
$this->app->make(AssigneeResolverRegistry::class),
'finance_team',
$this->app->make(FinanceTeamAssigneeResolver::class),
);
DBFlow::registerWorkflowHooks(
$this->app->make(WorkflowHooksRegistry::class),
'refund_dispute_approval',
RefundDisputeWorkflowHooks::class,
);
});
}
}
Register the provider in bootstrap/providers.php.
Core ships php artisan dbflow:sync and php artisan dbflow:validate. You may also call SyncWorkflowDefinitions from a deploy hook or custom Artisan command.
php artisan dbflow:sync
php artisan dbflow:sync --dry-run
php artisan dbflow:sync --workflow=refund_dispute_approval
php artisan dbflow:validate --strict
Programmatic alternative:
use DbflowLabs\Core\Actions\SyncWorkflowDefinitions;
/** @var array{created: list<string>, updated: list<string>, unchanged: list<string>} $summary */
$summary = app(SyncWorkflowDefinitions::class)->handle();
3. Sync definitions into the database
DBFlow::start() resolves the published version from dbflow_workflows / dbflow_workflow_versions. Registering a PHP provider alone is not enough.
Run sync:
- After first deploy
- After changing a
WorkflowDefinitionProviderdefinition - In CI before integration tests that call
DBFlow::start()
Provider definitions may include 'enabled' => true (maps to dbflow_workflows.is_enabled). Disabled workflows throw WorkflowNotAvailableException on start.
Filament definition editor after code sync
php artisan dbflow:sync publishes versions but does not write draft_definition. The Standard WorkflowResource node editor is hidden unless Workflow::hasDraft() is true — so code-synced workflows may show metadata on the edit page but no approval graph form.
If operators need the Standard editor for code-owned workflows, seed a draft after sync with SaveWorkflowDraft and the workflow's currentDefinition(). Skip workflows with source = ui or existing drafts. See Filament UI.
Alternatively, disable enable_workflow_definition_resource when definitions are code-only.
4. Assignee resolution (callback / permission)
Approval nodes declare assignees under config.assignees. The runtime supports:
type |
Runtime behaviour |
|---|---|
user |
Single user ID in value (string). |
callback |
callback (or value) must match a key passed to DBFlow::registerAssigneeResolver(). |
permission |
value is also a resolver registry key — not Spatie Permission, not Laravel Gate. |
role |
Listed in schema for editors; not supported by the open-core runtime (throws InvalidWorkflowDefinitionException). |
Example callback assignee in definition JSON:
'assignees' => [
'type' => 'callback',
'callback' => 'finance_team',
],
Resolver implementation:
use DbflowLabs\Core\Contracts\AssigneeResolver;
use DbflowLabs\Core\Models\WorkflowInstance;
final class FinanceTeamAssigneeResolver implements AssigneeResolver
{
public function resolve(WorkflowInstance $instance, array $nodeDefinition): array
{
// Return list<string> user IDs (int, UUID, or ULID keys as strings)
return [/* ... */];
}
}
See Code-defined Workflows for full examples.
5. Start workflows with metadata (optional)
DBFlow::start(
'refund_dispute_approval',
$dispute,
$submitter,
['submit_comment' => 'Please review'],
);
Metadata is stored on dbflow_workflow_instances.metadata. It does not replace WorkflowContextInterface::getWorkflowVariables() for transition conditions.
6. Act on tasks — always pass an actor
DBFlow::approve($task, $user, 'Approved.');
DBFlow::reject($task, $user, 'Missing receipt.', RejectStrategy::End);
When $actor is null, Core skips pending-assignment checks on approve. Production code should always pass the authenticated user.
7. Reassign — host authorization required
DBFlow::reassign($task, $fromActor, $toUserId, 'Handover to colleague.');
Core replaces a single pending assignment. It does not enforce who may reassign — gate reassign() in Filament actions or services. Filament Standard exposes a reassign action on My Tasks when enable_my_task_reassign_action is true.
8. Cancel — host authorization required
DBFlow::cancel($instance, $user, 'Withdrawn by submitter.');
Core does not enforce “only the submitter may cancel”. Gate cancel() in your Filament actions or services (for example compare $instance->started_by_user_id).
Cancelled instances set status = cancelled and clear active_key. Whether the business record may proceed after cancel is host policy (hooks or guards).
9. Query helpers on the model
HasWorkflow exposes:
$model->runningWorkflowInstance('workflow_key');
$model->completedWorkflowInstance('workflow_key'); // approved | rejected | cancelled
$model->hasRunningWorkflow('workflow_key');
$model->workflowLogs('workflow_key');
Use completedWorkflowInstance() when business actions (confirm order, post payment) must check the latest terminal workflow outcome.
10. UI options
| Approach | When to use |
|---|---|
dbflowlabs/filament |
Recommended Standard UI: My Tasks, instances, timelines, optional definition resource. Register via DBFlowFilamentPanel::register($panel) and replace permission_checker_class. |
| Host-built Filament inbox | Custom task list when you need a deeply embedded ERP UX; still call DBFlow::approve() / reject() with an authenticated actor. |
| API only | Mobile or external approvers. |
Core does not require Filament. Filament UI is a separate Composer package.
Filament Standard checklist (when using dbflowlabs/filament)
composer require dbflowlabs/filament:^1.0 dbflowlabs/core:^1.0; migrate.- Publish
dbflow-filament-config; setpermission_checker_class(notAllowAllPermissionChecker). - Call
DBFlowFilamentPanel::register($panel)when bothdbflow.enabledand your host feature flag are on. - Implement
WorkflowRouteResolvableon workflowable models for links back to business resources. - Bind
PermissionAssigneeOptionsResolverso definition editors show labels for your Core assignee resolver keys. - After sync, seed drafts if operators need the Standard definition editor for code workflows.
See Host Reference Patterns for a full module layout, boot order, and ERP pilot case study.
11. Business guards (host responsibility)
Core does not know your domain rules. Examples the host must implement:
- Block
confirm()whilehasRunningWorkflow()is true - Hide legacy approval UI when DBFlow engine is active
- Map hook outcomes to document status columns
Runtime exceptions (quick reference)
| Exception | Typical cause |
|---|---|
WorkflowNotFoundException |
Key missing — sync not run or typo |
WorkflowNotPublishedException |
No current_version_id / active version |
WorkflowNotAvailableException |
is_enabled = false or workflow archived |
WorkflowAlreadyRunningException |
Duplicate active_key for same workflowable |
InvalidWorkflowDefinitionException |
Invalid graph or empty assignee resolution |
UserCannotApproveTaskException |
Actor has no pending assignment |
TaskNotPendingException |
Stale task action / double submit |
Approval timeouts
When approval nodes include config.timeout.due_in (ISO 8601 duration) and on_timeout: reject_end, schedule:
php artisan dbflow:process-timeouts
Run every 5–15 minutes via cron or your scheduler. Filament Standard editors can set timeout fields; Core executes them.
UI binding mode (binding_mode = ui)
When DBFLOW_BINDING_MODE=ui, HasWorkflow auto-starts matching workflows on model created. You must also set dbflow_workflows.model_type to the workflowable morph class for each workflow row. php artisan dbflow:sync does not set model_type automatically — configure it in seeders or admin tooling.
What's next
- First Workflow — hands-on refund example
- Code-defined Workflows — JSON schema and assignees
- Approve and Reject — task API and audit logs
- Testing Workflows — PHPUnit patterns