---
title: "Recipes — Workflows for Laravel | Roundly"
description: "Five complete workflow files — purchase approval, the rework loop, onboarding, an SLA reminder, a nightly schedule — and a test that drives them."
url: https://roundly-consulting.com/open-source/docs/workflows-for-laravel/recipes
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)

# Recipes

Five complete workflows, each a file in the template envelope that workflows:sync reads (or versions()->import() imports). Every recipe is also a test fixture of the package, published and driven to its documented end state. Because they are the fixtures verbatim, every recipe uses the test app’s one registered subject type, "subject\_type": "document", with its title and amount fields (so the SLA recipe’s “resolved” check reads subject.title), and the onboarding and nightly recipes use an acme.create\_task host step. In your app, set subject\_type to the key you registered for the real model (for example purchase\_request or ticket) and point the field refs at that type’s registered fields; publish validation reports any field the subject type doesn’t have.

## Purchase approval with escalation

A submitted purchase request over 5,000 goes to its approver; after two business days without a decision the approval is reassigned to the approver’s manager, and it expires after five. Small requests skip the approval. Needs approvals-for-laravel and notifications-for-laravel:

```json
{
  "schema": "roundly.workflows.template/v1",
  "definition": {
    "key": "purchase-approval",
    "name": "Purchase approval",
    "subject_type": "document",
    "description": null
  },
  "graph": {
    "schema": "roundly.workflows.graph/v1",
    "settings": {"timezone": "Europe/Bratislava", "calendar": "weekdays"},
    "nodes": [
      {
        "id": "start",
        "type": "trigger",
        "config": {"trigger": "event", "event": "purchase_request.submitted"}
      },
      {
        "id": "big",
        "type": "condition",
        "config": {"when": {"field": "subject.amount", "op": "gt", "value": "5000"}}
      },
      {
        "id": "approve",
        "type": "approval",
        "config": {
          "approvers": [{"kind": "field", "field": "subject.approver"}],
          "rule": "any",
          "expires_in": {"amount": 5, "unit": "business_days"}
        },
        "escalations": [
          {
            "after": {"amount": 2, "unit": "business_days"},
            "actions": [
              {
                "type": "reassign",
                "to": [{"kind": "field", "field": "subject.approver_manager"}]
              }
            ]
          }
        ]
      },
      {
        "id": "mark",
        "type": "set_field",
        "config": {"field": "subject.priority", "value": "approved"}
      },
      {
        "id": "tell",
        "type": "notify",
        "config": {
          "notification": "purchase.rejected",
          "recipients": [{"kind": "field", "field": "subject.requester"}]
        }
      },
      {"id": "done", "type": "end"}
    ],
    "edges": [
      {"from": "start", "to": "big"},
      {"from": "big", "to": "approve", "outcome": "true"},
      {"from": "big", "to": "mark", "outcome": "false"},
      {"from": "approve", "to": "mark", "outcome": "approved"},
      {"from": "approve", "to": "tell", "outcome": "rejected"},
      {"from": "approve", "to": "tell", "outcome": "expired"},
      {"from": "mark", "to": "done"},
      {"from": "tell", "to": "done"}
    ]
  }
}
```

## The rework loop

A cycle is legal only through a delay or loop node, so “approval → rework → approval again” goes through a loop that caps the rounds. Each pass through approve opens a fresh round with a fresh idempotency key. Needs approvals-for-laravel:

```json
{
  "schema": "roundly.workflows.template/v1",
  "definition": {"key": "rework-loop", "name": "Rework loop", "subject_type": "document", "description": null},
  "graph": {
    "schema": "roundly.workflows.graph/v1",
    "nodes": [
      {
        "id": "start",
        "type": "trigger",
        "config": {"trigger": "event", "event": "purchase_request.submitted"}
      },
      {"id": "round", "type": "loop", "config": {"max": 3}},
      {
        "id": "approve",
        "type": "approval",
        "config": {"approvers": [{"kind": "field", "field": "subject.approver"}], "rule": "any"}
      },
      {
        "id": "rework",
        "type": "wait",
        "config": {"event": "purchase_request.resubmitted", "timeout": "P14D"}
      },
      {"id": "done", "type": "end"},
      {
        "id": "gave_up",
        "type": "end",
        "config": {"status": "failed", "reason": "too many rounds"}
      }
    ],
    "edges": [
      {"from": "start", "to": "round"},
      {"from": "round", "to": "approve", "outcome": "continue"},
      {"from": "round", "to": "gave_up", "outcome": "exhausted"},
      {"from": "approve", "to": "done", "outcome": "approved"},
      {"from": "approve", "to": "rework", "outcome": "rejected"},
      {"from": "approve", "to": "gave_up", "outcome": "expired"},
      {"from": "rework", "to": "round", "outcome": "received"},
      {"from": "rework", "to": "gave_up", "outcome": "timed_out"}
    ]
  }
}
```

## Onboarding checklist

A subject-less manual workflow: two host steps and a child workflow it waits for. The child, welcome, is any subject-less manual definition of the same owner; start\_workflow refuses one whose required inputs it doesn’t cover:

```json
{
  "schema": "roundly.workflows.template/v1",
  "definition": {
    "key": "onboarding",
    "name": "Onboarding checklist",
    "subject_type": null,
    "description": null
  },
  "graph": {
    "schema": "roundly.workflows.graph/v1",
    "nodes": [
      {
        "id": "start",
        "type": "trigger",
        "config": {
          "trigger": "manual",
          "inputs": [{"name": "employee", "type": "string", "label": "Employee", "required": true}]
        }
      },
      {"id": "laptop", "type": "acme.create_task", "config": {"title": "Order a laptop"}},
      {"id": "accounts", "type": "acme.create_task", "config": {"title": "Create the accounts"}},
      {
        "id": "welcome",
        "type": "start_workflow",
        "config": {"definition": "welcome", "subject": null, "wait": true}
      },
      {"id": "done", "type": "end"},
      {
        "id": "failed",
        "type": "end",
        "config": {"status": "failed", "reason": "The welcome did not finish"}
      }
    ],
    "edges": [
      {"from": "start", "to": "laptop"},
      {"from": "laptop", "to": "accounts"},
      {"from": "accounts", "to": "welcome"},
      {"from": "welcome", "to": "done", "outcome": "completed"},
      {"from": "welcome", "to": "failed", "outcome": "failed"}
    ]
  }
}
```

## SLA reminder

Waits until the ticket is resolved — re-checked whenever the subject changes (the HasWorkflows trait) and every 15 minutes — and escalates after four business hours by starting a sla-chase workflow. After five days the SLA is breached:

```json
{
  "schema": "roundly.workflows.template/v1",
  "definition": {
    "key": "sla-reminder",
    "name": "SLA reminder",
    "subject_type": "document",
    "description": null
  },
  "graph": {
    "schema": "roundly.workflows.graph/v1",
    "settings": {"timezone": "UTC", "calendar": "weekdays"},
    "nodes": [
      {
        "id": "start",
        "type": "trigger",
        "config": {"trigger": "event", "event": "ticket.opened"}
      },
      {
        "id": "resolved",
        "type": "wait",
        "config": {
          "condition": {"field": "subject.title", "op": "equals", "value": "Resolved"},
          "poll": "PT15M",
          "timeout": "P5D"
        },
        "escalations": [
          {
            "after": {"amount": 4, "unit": "business_hours"},
            "actions": [{"type": "start_workflow", "definition": "sla-chase"}]
          }
        ]
      },
      {"id": "done", "type": "end"},
      {
        "id": "breached",
        "type": "end",
        "config": {"status": "failed", "reason": "SLA breached"}
      }
    ],
    "edges": [
      {"from": "start", "to": "resolved"},
      {"from": "resolved", "to": "done", "outcome": "met"},
      {"from": "resolved", "to": "breached", "outcome": "timed_out"}
    ]
  }
}
```

## Nightly schedule

A subject-less schedule: every night at 02:00 Bratislava time, one instance. Missed nights within schedule.catch\_up start once; a repeated DST hour runs once:

```json
{
  "schema": "roundly.workflows.template/v1",
  "definition": {"key": "nightly", "name": "Nightly clean-up", "subject_type": null, "description": null},
  "graph": {
    "schema": "roundly.workflows.graph/v1",
    "nodes": [
      {
        "id": "start",
        "type": "trigger",
        "config": {"trigger": "schedule", "cron": "0 2 * * *", "timezone": "Europe/Bratislava"}
      },
      {"id": "report", "type": "acme.create_task", "config": {"title": "Nightly report"}},
      {"id": "done", "type": "end"}
    ],
    "edges": [{"from": "start", "to": "report"}, {"from": "report", "to": "done"}]
  }
}
```

## Driving them in a test

```php
uses(InteractsWithWorkflows::class);

$this->fakeStep('approval', fn (StepContext $c) => StepResult::complete($c->visit() === 1 ? 'rejected' : 'approved'),
    outcomes: ['approved', 'rejected', 'expired']);
Workflows::definitions()->sync(resource_path('workflows'), publish: true);

Workflows::emit('purchase_request.submitted', $request);
Workflows::emit('purchase_request.resubmitted', $request);

expect($request->workflowInstances()->sole())->toHaveCompletedVia(['start', 'round', 'approve', 'rework', 'round', 'approve', 'done']);
```

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