---
title: "Database — Workflows for Laravel | Roundly"
description: "The eight tables — every column, index and foreign key, how key types size the morph columns, and what workflows:prune deletes after retention."
url: https://roundly-consulting.com/open-source/docs/workflows-for-laravel/database
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Workflows for Laravel](https://roundly-consulting.com/open-source/docs/workflows-for-laravel.md)

# Database

Eight tables, published by workflows:install (tag workflows-migrations, timestamped on publish; never auto-loaded). Names come from workflows.tables.\*. Migrations are forward-only. Key types are fixed at migration time — set them before php artisan migrate:

- id and every package-internal key column (\*\_id pointing at a package table) follow primary\_key\_type (bigint auto-increment, or a time-ordered uuid / ulid primary key).
- A morph pair <name>\_type + <name>\_id (nullable) holds a host model. Its id column follows owner\_key\_type (owner\_\*), subject\_key\_type (subject\_\*) or key\_type (publisher\_\*, starter\_\*, requester\_\*).
- Every timestamp is stored in UTC (created\_at, updated\_at, and deleted\_at on soft-deleting tables).

## workflow\_definitions

| Column | Type | Notes |
| --- | --- | --- |
| `id` | key | — |
| `key` | string(100) | Unique per owner (wdef\_key\_unique on owner\_scope, key). |
| `name` | string(200) | — |
| `description` | text, nullable | — |
| `owner_type, owner_id` | morph, nullable | Null = app-wide. |
| `owner_scope` | string(191) | The owner as one string (<morph class>:<key>, \* for app-wide), for the unique key. |
| `subject_key` | string(100), nullable | The subject type’s registry key (never a class name). |
| `status` | string(16) | DefinitionStatus. |
| `current_version_id` | key, nullable, indexed | The published version new instances run. |
| `webhook_id` | string(26), nullable, unique | The inbound hook id (minted at first publish, rotatable). |
| `timestamps, deleted_at` | | Soft deletes; a trashed definition still holds its key. |

## workflow\_versions

| Column | Type | Notes |
| --- | --- | --- |
| `definition_id` | key | FK → definitions, restrict on delete. |
| `number` | unsigned int | Unique per definition (wver\_number\_unique). |
| `status` | string(16) | VersionStatus. |
| `schema_version` | string(32) | roundly.workflows.graph/v1 |
| `graph / trigger / settings` | json | The whole document, the trigger node’s config, the settings. |
| `trigger_type / trigger_key` | string(16) / string(191), nullable | Indexed with status (wver\_trigger\_idx) for emit and webhook lookups. |
| `actor_mode` | string(16) | ActorMode. |
| `checksum` | char(64) | The canonical graph digest (expectedChecksum, sync, export). |
| `review_flags` | json, nullable | Open template review flags. |
| `published_at` | datetime, nullable | Once set, graph, trigger, actor, settings, checksum and publisher are frozen. |
| `publisher_type, publisher_id` | morph, nullable | Who published. |
| `schedule_cursor_at` | datetime, nullable | The last schedule slot handled. |

## workflow\_instances

| Column | Type | Notes |
| --- | --- | --- |
| `ulid` | char(26), unique | The public id (resources, commands, idempotency keys). |
| `definition_id / version_id` | key | FKs → definitions / versions; the version never changes. |
| `owner_type, owner_id` | morph, nullable | The instance owner. |
| `subject_key, subject_type, subject_id` | string(100) / morph, nullable | Registry key and the subject. |
| `trigger_type / trigger_key / trigger_payload` | string / json, nullable | How it started. |
| `idempotency_digest` | char(64), nullable, indexed | — |
| `status` | string(16) | InstanceStatus. |
| `current_node_id` | string(64), nullable | — |
| `steps_count` | unsigned int, default 0 | Counted against limits.max\_steps. |
| `depth` | unsigned tinyint, default 0 | start\_workflow nesting. |
| `parent_instance_id, parent_step_run_id, root_instance_id` | key, nullable | The parent (FK, null on delete) and the root. |
| `actor_mode, starter_type, starter_id` | string(16) / morph, nullable | The actor and the triggering user. |
| `failure_code / failure_message / failed_node_id` | string(64) / text / string(64), nullable | A FailureCode value or a host code; the message is stored for operators. |
| `started_at, ended_at` | datetime, nullable | — |
| `lock_version` | unsigned int, default 0 | Bumped on every state change. |

## workflow\_step\_runs

| Column | Type | Notes |
| --- | --- | --- |
| `instance_id` | key | FK → instances, cascade on delete. |
| `node_id / node_type` | string(64) | — |
| `visit / attempt` | unsigned int / unsigned smallint | Unique with instance\_id, node\_id (wstep\_attempt\_unique). |
| `status` | string(16) | StepRunStatus. |
| `idempotency_key` | string(100) | wf:{instance ulid}:{node id}:{visit} |
| `input / output` | json, nullable | The resolved config the handler saw (capped by steps\_input.max\_bytes) and its output. |
| `resume_signal` | json, nullable | The signal the attempt was last resumed with; a retry of a failed resume carries it. |
| `outcome` | string(32), nullable | — |
| `error_code / error_message / error_retryable` | string(64) / text / bool, nullable | — |
| `claim_token / lease_until` | string(26) / datetime, nullable | The compare-and-set claim and its lease (wstep\_lease\_idx). |
| `scheduled_for, dispatched_at, started_at, finished_at, probe_pushed_at` | datetime, nullable | Timings; a pushed probe not claimed yet is re-pushed by wake. |

## workflow\_waits

| Column | Type | Notes |
| --- | --- | --- |
| `instance_id / step_run_id` | key | FKs, cascade on delete. |
| `kind` | string(16) | WaitKind. |
| `due_at` | datetime, nullable | wwait\_due\_idx (status, due\_at). |
| `event_key` | string(191), nullable | wwait\_event\_idx (status, event\_key). |
| `match / condition / payload` | json, nullable | Event matching, condition, delivered payload. |
| `outcome` | string(32), nullable | A timeout’s outcome. |
| `status, claim_token, claimed_at, fired_at` | | WaitStatus and the claim. |

## workflow\_escalations

| Column | Type | Notes |
| --- | --- | --- |
| `instance_id / step_run_id` | key | FKs, cascade on delete. |
| `level` | unsigned smallint | 1 = the first. |
| `due_at` | datetime | wesc\_due\_idx (status, due\_at). |
| `actions` | json | — |
| `status, attempts` | string(16) / unsigned smallint | EscalationStatus and attempts made. |
| `claim_token, claimed_at, fired_at, cancelled_at, error_code, error_message` | | — |

## workflow\_idempotency\_keys

| Column | Type | Notes |
| --- | --- | --- |
| `scope` | string(16) | start or effect. |
| `key` | char(64) | A digest, never the raw key; unique with scope (widem\_unique). |
| `definition_id / instance_id` | key, nullable | FKs (null / cascade on delete). |
| `result` | json, nullable | A once() result. |
| `expires_at` | datetime, nullable, indexed | Trigger keys only (idempotency.ttl). |
| `created_at` | datetime | No soft deletes. |

## workflow\_dry\_runs

| Column | Type | Notes |
| --- | --- | --- |
| `definition_id / version_id` | key / key, nullable | FKs (cascade / null on delete). |
| `owner_*, subject_*, requester_*` | morphs, nullable | — |
| `input / result` | json | The input and the DryRunResult. |
| `timestamps, deleted_at` | | Pruned after prune.dry\_runs\_after. |

workflow\_instances also has winst\_overlay\_idx (version\_id, status, current\_node\_id) for the run overlay, winst\_rate\_idx (definition\_id, created\_at) for the start rate limit and winst\_status\_idx (status, updated\_at) for the doctor’s stuck check.

## Retention

workflows:prune (schedule daily) force-deletes ended instances older than prune.instances\_after — never one with a running child — with their step runs, waits, escalations and effect keys; expired trigger keys; and dry-run logs older than prune.dry\_runs\_after.

## 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](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

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

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/workflows-for-laravel.md)
