---
title: "Subjects, owners and parties — Workflows for Laravel | Roundly"
description: "Register subject types with typed fields, resolve owners, use the HasWorkflows trait, and address approvers and recipients as party references."
url: https://roundly-consulting.com/open-source/docs/workflows-for-laravel/subjects-and-parties
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)

# Subjects, owners and parties

## Subject types

A subject is the model an instance is about. Register each type once, in a service provider’s boot(). Fields are what conditions, refs and set\_field may read and write — nothing else on the model:

```php
use RoundlyConsulting\Workflows\Facades\Workflows;

Workflows::subjects()->register(
    Workflows::subject('purchase_request', PurchaseRequest::class)   // SubjectTypeBuilder
        ->label('Purchase request')
        ->ownedBy('team')                     // relation or attribute naming the owner
        ->field('amount', 'decimal', 'Amount')
        ->field('status', 'enum', 'Status', options: PurchaseStatus::class)   // enum class or list
        ->field('requester', 'party', 'Requester')                             // relation to a user
        ->field('approver_manager', 'party', "Approver's manager", path: 'approver.manager')
        ->field('due_on', 'date', 'Due on')
        ->field('priority', 'string', 'Priority', writable: true)              // set_field may write it
        ->ability('update'),                  // the ability set_field checks on this type
);
```

- Field types: string, text, integer, decimal, boolean, date, datetime, enum, party, list. Names match ^\[a-z\]\[a-z0-9\_\]{0,63}$.
- Reads go through the registered path (the attribute, or a dotted relation). Unregistered attributes are never read. Enums read as their backed value; dates normalise to the version timezone (a date at midnight keeps its stored calendar day); decimals read as strings.
- The registry key (purchase\_request), never the class name, is what definitions, templates, resources and the API store and show.
- A definition may have no subject type (subjectType: null): its instances run without a subject.

Instead of registering in a provider, list classes implementing Contracts\\DescribesWorkflowSubject in workflows.subjects:

```php
use RoundlyConsulting\Workflows\Builders\SubjectTypeBuilder;
use RoundlyConsulting\Workflows\Contracts\DescribesWorkflowSubject;
use RoundlyConsulting\Workflows\Facades\Workflows;

final class PurchaseRequestSubject implements DescribesWorkflowSubject
{
    public function describe(): SubjectTypeBuilder
    {
        return Workflows::subject('purchase_request', PurchaseRequest::class)
            ->label('Purchase request')
            ->ownedBy('team')
            ->field('amount', 'decimal', 'Amount');
    }
}

// config/workflows.php
'subjects' => [App\Workflows\PurchaseRequestSubject::class],
```

The registry is what a builder’s field picker reads:

```php
Workflows::subjects()->find('purchase_request');     // ?SubjectTypeDefinition
Workflows::subjects()->all();                        // array<string, SubjectTypeDefinition>
Workflows::subjects()->export('purchase_request');   // key, label, fields with type, label, writable, options
```

## The HasWorkflows trait

```php
$request->workflows()->start('purchase-approval', $team)->dispatch();
$request->workflows()->emit('purchase_request.submitted', ['note' => 'urgent']);
$request->workflows()->instances();     // cursor-paginated history, newest first
$request->workflows()->running();       // running or waiting instances
$request->workflows()->cancelAll('withdrawn');
$request->workflows()->touch();         // re-evaluate its condition waits
$request->workflowInstances();          // the morphMany relation

// The same handle without the trait:
Workflows::for($request)->running();
```

After an update commits, the trait touches the subject if it has pending condition waits — but only when the update changed a registered field’s attribute or a belongs-to field’s foreign key, so ordinary saves add no query. After a delete, subjects\_on\_deleted = cancel (default) cancels its running instances; keep leaves them (they fail subject\_missing when they next need it). Everything goes through the manager, so Workflows::fake() sees it.

## Owners

The owner is the tenant a definition belongs to — a team, a company. Null = app-wide. A subject’s owner is resolved, in order, from:

- Contracts\\HasWorkflowOwner::workflowOwner() on the subject;
- the subject type’s ownedBy(relation|attribute);
- the teams bridge (BelongsToTeam / ScopedToTeam: the team column);
- otherwise unknown.

```php
use Illuminate\Database\Eloquent\Model;
use RoundlyConsulting\Workflows\Contracts\HasWorkflowOwner;

class PurchaseRequest extends Model implements HasWorkflowOwner
{
    public function workflowOwner(): ?Model
    {
        return $this->department->company;   // checked before ownedBy() and the teams bridge
    }
}
```

An app-wide definition accepts any subject. An owned definition accepts a subject only when its owner has the same morph class and key; an unknown owner is a mismatch (fail closed). An owned definition whose subject type has no owner resolution can’t be published (subject\_owner\_unresolvable).

The instance owner is the definition’s owner, or — for an app-wide definition — the subject’s resolved owner at start. Every later check and the owner context use the instance owner. A subject that moves to another owner mid-run fails the next step with owner\_mismatch before its handler runs. Owner types register like subjects, so owner.<field> refs validate:

```php
// Owner types register like subjects, so owner.<field> refs validate.
Workflows::subjects()->register(
    Workflows::subject('team', Team::class)->ownerType()->field('plan', 'string', 'Plan'),
);
```

## Parties

Approvers, recipients and escalation targets are party references, discriminated by kind:

```json
{"kind": "user", "id": 12}
{"kind": "field", "field": "subject.requester"}
{"kind": "role", "role": "legal"}
{"kind": "triggered_by"}
```

| Kind | JSON | Resolves to | Owner-specific |
| --- | --- | --- | --- |
| `user` | `{"kind":"user","id":12}` | The user model (workflows.parties.user\_model, default auth.providers.users.model). | yes |
| `field` | `{"kind":"field","field":"subject.requester"}` | A registered party field of the subject or owner. | no |
| `role` | `{"kind":"role","role":"legal"}` | ResolvesRoles::members($owner, $role) — the teams bridge, or workflows.parties.roles. | no |
| `triggered_by` | `{"kind":"triggered_by"}` | The instance’s starter. | no |

- Membership: user, field and triggered\_by parties pass Contracts\\ChecksOwnerMembership::isMember($owner, $user) (workflows.owners.membership; the teams bridge requires an active membership; without one, everyone is a member).
- Without a roles resolver, role parties are refused at publish (roles\_unavailable).
- Duplicates are removed by key; soft-deleted users are skipped.
- An empty resolution is never “anyone”: an approval stage that resolves to nobody fails no\_approvers; a notify step with nobody to notify completes with recipients: 0.
- Owner-specific values (user parties) are cleared and flagged for review when a template is imported for another owner.

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