<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://durable-workflow.com/uk/blog/</id>
    <title>Durable Workflow Blog</title>
    <updated>2026-09-29T00:00:00.000Z</updated>
    <generator>https://github.com/jpmonette/feed</generator>
    <link rel="alternate" href="https://durable-workflow.com/uk/blog/"/>
    <subtitle>Durable Workflow Blog</subtitle>
    <icon>https://durable-workflow.com/uk/img/favicon.ico</icon>
    <entry>
        <title type="html"><![CDATA[What We Learned Testing Durable Workflow Server's HTTP Stacks]]></title>
        <id>https://durable-workflow.com/uk/blog/what-we-learned-testing-server-stacks/</id>
        <link href="https://durable-workflow.com/uk/blog/what-we-learned-testing-server-stacks/"/>
        <updated>2026-09-29T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Which HTTP stack should serve Durable Workflow Server? We measured Apache,]]></summary>
        <content type="html"><![CDATA[<p>Which HTTP stack should serve Durable Workflow Server? We measured Apache,
PHP-FPM, and persistent PHP workers against the same application workload. The
answer depends on the whole request path, including the time SDK workers spend
waiting for tasks. Here are the results, what they mean for our Server, and why
a different application might choose differently.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-results">The results<a href="https://durable-workflow.com/uk/blog/what-we-learned-testing-server-stacks/#the-results" class="hash-link" aria-label="Пряме посилання на The results" title="Пряме посилання на The results" translate="no">​</a></h2>
<p>We tested six ways to serve the same Laravel application. In a two-hour run,
each stack handled short API requests while twelve synthetic workers kept a
mix of workflow, activity, and query polls active. The HTTP side had one CPU
and 1 GiB of memory. MySQL, Redis, the queue worker, and SDK workers had their
own fixed limits. These runs used published Server 2.4.15, PHP 8.3.35, and
the same standard one-activity workflow. Every completed workflow had its
result and ordered history checked.</p>
<table><thead><tr><th>HTTP model</th><th style="text-align:right">Workflows completed</th><th style="text-align:right">Workflow p95</th><th style="text-align:right">Peak HTTP memory</th><th>Ordinary API gate</th></tr></thead><tbody><tr><td>Apache prefork + mod_php</td><td style="text-align:right">811/811</td><td style="text-align:right">11.62 s</td><td style="text-align:right">138.5 MiB</td><td>Passed</td></tr><tr><td>nginx + PHP-FPM</td><td style="text-align:right">790/790</td><td style="text-align:right">13.12 s</td><td style="text-align:right">159.9 MiB</td><td>Passed</td></tr><tr><td>Apache event + PHP-FPM</td><td style="text-align:right">788/788</td><td style="text-align:right">13.34 s</td><td style="text-align:right">181.6 MiB</td><td>One readiness failure</td></tr><tr><td>Octane + FrankenPHP</td><td style="text-align:right">857/857</td><td style="text-align:right">11.87 s</td><td style="text-align:right">304.2 MiB</td><td>Passed</td></tr><tr><td>Octane + Swoole</td><td style="text-align:right">684/684</td><td style="text-align:right">13.15 s</td><td style="text-align:right">755.5 MiB</td><td>Timed out</td></tr><tr><td>Octane + OpenSwoole</td><td style="text-align:right">702/702</td><td style="text-align:right">12.77 s</td><td style="text-align:right">749.2 MiB</td><td>Timed out</td></tr></tbody></table>
<p>The two-hour runs test sustained behavior and whether ordinary API calls
remain available. Their closed-loop workflow totals are not a maximum
throughput ranking. Apache, nginx/FPM, and FrankenPHP passed that gate.
Apache event/FPM had a readiness failure. Swoole and OpenSwoole completed
their workflows but timed out ordinary requests under this load.</p>
<p>We also repeated a fixed-rate workload on the newer published Server 2.4.26
application, bundled Workflow 2.2.18, and PHP SDK 2.1.5. Two SDK workers
offered 75 workflows at 1.25 starts per second for 60 seconds. Each run
included a measured drain, so a late completion did not count as work
finished during the offer.</p>
<table><thead><tr><th>HTTP model</th><th style="text-align:right">Completed during the 60-second offer</th><th style="text-align:right">Completed after drain</th><th style="text-align:right">HTTP CPU per completed workflow</th><th style="text-align:right">HTTP memory after load</th></tr></thead><tbody><tr><td>Apache</td><td style="text-align:right">37 to 38 of 75</td><td style="text-align:right">75 of 75</td><td style="text-align:right">1.20 to 1.22 s</td><td style="text-align:right">About 79 MiB</td></tr><tr><td>Swoole, eight workers</td><td style="text-align:right">53 to 61 of 75</td><td style="text-align:right">75 of 75</td><td style="text-align:right">0.90 to 0.97 s</td><td style="text-align:right">365 to 390 MiB</td></tr><tr><td>OpenSwoole, four workers</td><td style="text-align:right">56 to 62 of 75</td><td style="text-align:right">75 of 75</td><td style="text-align:right">0.89 to 0.92 s</td><td style="text-align:right">289 to 304 MiB</td></tr></tbody></table>
<p>That is a real speed advantage for the coroutine runtimes in this local
workload, with a substantial memory cost. Apache's three recheck windows
finished 37 to 38 workflows during the offer. The Swoole and OpenSwoole
ranges show their variation across three windows each. All work completed,
with no final backlog, swap, or result errors. A larger host might make the
memory trade worthwhile. On a small all-in-one Server, HTTP memory also has
to leave room for MySQL, Redis, the queue worker, and the operating system.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-a-standalone-server-is-still-a-laravel-app">Why a standalone Server is still a Laravel app<a href="https://durable-workflow.com/uk/blog/what-we-learned-testing-server-stacks/#why-a-standalone-server-is-still-a-laravel-app" class="hash-link" aria-label="Пряме посилання на Why a standalone Server is still a Laravel app" title="Пряме посилання на Why a standalone Server is still a Laravel app" translate="no">​</a></h2>
<p>Durable Workflow Server presents a language-neutral HTTP protocol to PHP,
Python, and Rust SDKs. Its implementation is a Laravel application. Laravel
handles its API routes and middleware. The published stack runs separate HTTP,
queue worker, and scheduler processes. The queue worker advances durable work.
MySQL holds durable state. Redis supports the queue, cache, and wake signals.
The HTTP process accepts starts and completions, and serves workflow, activity,
and query polls to SDK workers.</p>
<p>That structure matters when choosing the front end. An ordinary short API
request enters Laravel and leaves. A long poll can wait for work inside
<code>LongPoller</code>. In the current implementation, that wait occupies the PHP
execution slot handling the request. A web server may accept many connections,
but it cannot make a finite pool of busy PHP workers unlimited. The process limit,
memory per waiting worker, wakeup path, and responsiveness of ordinary API
requests matter more than the web server's name.</p>
<p>The candidates change how those slots are provided:</p>
<table><thead><tr><th>Model</th><th>What serves Laravel requests</th><th>Operational question</th></tr></thead><tbody><tr><td>Apache prefork + mod_php</td><td>Apache child processes run PHP</td><td>How many children fit in memory, and when should they recycle?</td></tr><tr><td>nginx + PHP-FPM</td><td>nginx proxies to a separate PHP process pool</td><td>Does separating connections from PHP work help enough to justify another process and configuration?</td></tr><tr><td>Apache event + PHP-FPM</td><td>Apache event MPM proxies to PHP-FPM</td><td>Does its connection handling change the result for this workload?</td></tr><tr><td>Octane + FrankenPHP, Swoole, or OpenSwoole</td><td>Long-lived PHP application workers</td><td>Can we gain capacity while preserving request isolation, recovery, and a bounded worker lifetime?</td></tr></tbody></table>
<p>Persistent workers also keep Laravel state in memory between requests. That
can remove repeated bootstrap work, but it makes cross-request state reset,
authentication isolation, stale backend connections, and clean recycling part
of the stack choice.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-else-moved-the-numbers">What else moved the numbers<a href="https://durable-workflow.com/uk/blog/what-we-learned-testing-server-stacks/#what-else-moved-the-numbers" class="hash-link" aria-label="Пряме посилання на What else moved the numbers" title="Пряме посилання на What else moved the numbers" translate="no">​</a></h2>
<p>Workers must wake for the right task. Registering a query poll was needlessly
waking workflow and activity polls, so we separated their wake signals. In a
local six-run comparison, spaced activity schedule-to-start fell from <strong>3.65
to 3.88 seconds</strong> in five of six old-worker runs to <strong>86 to 184 milliseconds</strong>
in all six new-worker runs.</p>
<p>We also made readiness detect an unavailable Redis queue and made the
published queue worker restart after a backend interruption. These changes
affect whether a fast Server stays useful through a failure. On the client
side, a worker-count check showed that one SDK worker limited the load test
while four contended for the fixed CPU budget. We sized the test client to
exercise the Server without turning it into the bottleneck.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-we-chose-and-what-could-change-the-answer">What we chose, and what could change the answer<a href="https://durable-workflow.com/uk/blog/what-we-learned-testing-server-stacks/#what-we-chose-and-what-could-change-the-answer" class="hash-link" aria-label="Пряме посилання на What we chose, and what could change the answer" title="Пряме посилання на What we chose, and what could change the answer" translate="no">​</a></h2>
<p>We kept Apache prefork with mod_php as the Server default. It passed the
two-hour mixed-request gate, kept the smallest HTTP memory footprint of the
passing stacks, and needs the fewest extra moving parts. The faster Swoole
and OpenSwoole local runs are valuable evidence, but their memory use and
ordinary API timeouts kept them out of this small-host default.</p>
<p>We then tuned the selected Apache image. The released Server 2.4.27 image
removes build-time Git while keeping the PHP extensions, Node and Python
tools used by the published conformance runner, and the existing Apache and
OPcache settings. Its compressed layers are 19.5 MB smaller on both amd64
and arm64. The amd64 visible root filesystem is about 32 MB smaller. Three
repeated fixed-load runs and a fresh baseline check showed similar completed
work, CPU use, latency, and memory. The gain is less data to transfer and
store, with the same application behavior. The exact published image passed
PHP, Python, and Rust lifecycle checks, plus recovery after MySQL and Redis
interruptions.</p>
<p>Another application can get a different result. An API with short, CPU-heavy
requests and few idle polls has a different worker-occupancy pattern. A site
whose PHP bootstrap dominates every response could benefit more from
persistent workers. A service with ample memory but tight CPU limits, or one
whose team already operates PHP-FPM, may value the tradeoffs differently. The
useful method is to freeze the application and workload, measure completed
work against total resources, and make readiness and recovery part of the
decision.</p>
<p>The full methods, results, and raw artifacts are in
<a href="https://github.com/durable-workflow/server/issues/137#issuecomment-5892913410" target="_blank" rel="noopener noreferrer" class="">the public engineering report</a>.</p>]]></content>
        <author>
            <name>Richard</name>
            <uri>https://github.com/rmcdaniel</uri>
        </author>
        <category label="server" term="server"/>
        <category label="performance" term="performance"/>
        <category label="php" term="php"/>
        <category label="laravel" term="laravel"/>
        <category label="reliability" term="reliability"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Introducing Durable Workflow 2.0: Durable Execution for the Polyglot, Agentic Era]]></title>
        <id>https://durable-workflow.com/uk/blog/durable-workflow-2-0/</id>
        <link href="https://durable-workflow.com/uk/blog/durable-workflow-2-0/"/>
        <updated>2026-09-01T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Today we are announcing Durable Workflow 2.0, the evolution of the PHP-native]]></summary>
        <content type="html"><![CDATA[<p>Today we are announcing Durable Workflow 2.0, the evolution of the PHP-native
project formerly known as Laravel Workflow into a standalone, polyglot durable
execution platform.</p>
<p>Modern systems rarely live in one language or one process. PHP may own the
application, Python the data and AI workloads, and Rust the performance-critical
services. Durable Workflow 2.0 gives those systems one durable execution model,
one protocol, and one operational surface. It is built for software developed
and operated by people and, increasingly, by autonomous agents.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="one-runtime-three-first-party-sdks">One runtime, three first-party SDKs<a href="https://durable-workflow.com/uk/blog/durable-workflow-2-0/#one-runtime-three-first-party-sdks" class="hash-link" aria-label="Пряме посилання на One runtime, three first-party SDKs" title="Пряме посилання на One runtime, three first-party SDKs" translate="no">​</a></h2>
<p>Durable Workflow 2.0 launches with first-party SDKs for
<a class="" href="https://durable-workflow.com/uk/docs/polyglot/php/">PHP</a>, <a class="" href="https://durable-workflow.com/uk/docs/polyglot/python/">Python</a>, and
<a class="" href="https://durable-workflow.com/uk/docs/polyglot/rust/">Rust</a>. They all communicate with the same
language-neutral runtime.</p>
<p>A PHP workflow can schedule an activity implemented by a Rust worker. A Python
client can start it. An operator can inspect the same execution through the CLI
or Waterline. Workflow and activity types cross language boundaries by public
name, and values cross them through a shared typed Avro protocol instead of
framework-specific object serialization.</p>
<p>The protocol uses one recursive Avro value schema for primitives, bytes,
strings, lists, and string-keyed maps. Values are encoded directly as Avro
datums, so distinctions such as integer versus floating-point values survive
round trips across all three SDKs without requiring every application to
maintain a schema for each workflow type.</p>
<p>For a team with a mixed stack, the workflow engine no longer has to be chosen
around the implementation language of one service.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="deploy-it-the-way-your-system-needs">Deploy it the way your system needs<a href="https://durable-workflow.com/uk/blog/durable-workflow-2-0/#deploy-it-the-way-your-system-needs" class="hash-link" aria-label="Пряме посилання на Deploy it the way your system needs" title="Пряме посилання на Deploy it the way your system needs" translate="no">​</a></h2>
<p>The 2.0 runtime owns durable state, command and history recording, task
matching, timers, schedules, namespaces, and recovery. Application workers
connect to it and scale independently in PHP, Python, or Rust.</p>
<p>There are three supported ways to run it:</p>
<ul>
<li class=""><strong>Durable Workflow Cloud</strong> operates the runtime for you. Your clients and
workers connect to a managed namespace; you do not run Durable Workflow
Server.</li>
<li class=""><strong>Standalone Server</strong> gives self-hosted teams the same language-neutral
runtime boundary in a published container.</li>
<li class=""><strong>Embedded Laravel</strong> keeps the original Laravel-native model for teams that
want the workflow engine inside the application they already deploy.</li>
</ul>
<p>Embedded Laravel remains a first-class product surface. It is now one option
within a broader platform rather than the limit of where Durable Workflow can
run.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="built-and-hardened-in-public">Built and hardened in public<a href="https://durable-workflow.com/uk/blog/durable-workflow-2-0/#built-and-hardened-in-public" class="hash-link" aria-label="Пряме посилання на Built and hardened in public" title="Пряме посилання на Built and hardened in public" translate="no">​</a></h2>
<p>Durable Workflow 2.0 is developed with coding agents that implement changes,
write tests, triage failures, and prepare releases. A human product owner sets
direction and retains stable-release authority. GitHub issues, pull requests,
commits, and Actions checks keep that work visible and approachable to other
contributors.</p>
<p>Release qualification exercises installable packages from Packagist, PyPI,
and crates.io together with published Docker images. That work has found
defects that ordinary happy-path tests missed:</p>
<ul>
<li class="">A <a href="https://github.com/durable-workflow/sdk-python/commit/071c1b6e0fee5fd6830f20babcf75b3d694da5c5" target="_blank" rel="noopener noreferrer" class="">Python replay defect</a>
mistook a condition timeout timer for an activity during replay and left the
workflow unable to complete.</li>
<li class="">A <a href="https://github.com/durable-workflow/sdk-python/commit/7cbd354" target="_blank" rel="noopener noreferrer" class="">Python worker contract defect</a>
omitted workflow update declarations, causing the live runtime to reject
valid updates as unknown.</li>
<li class="">A <a href="https://github.com/durable-workflow/server/commit/4459eae0" target="_blank" rel="noopener noreferrer" class="">server heartbeat race</a>
allowed timeout enforcement to race with a newer accepted activity
heartbeat.</li>
</ul>
<p>Each defect was reproduced, fixed, covered by a regression test, and verified
against a new release candidate. Agentic development does not remove
engineering discipline. It shortens the loop between a concrete failure, a
reviewable fix, and public verification.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="agent-operable-by-design">Agent-operable by design<a href="https://durable-workflow.com/uk/blog/durable-workflow-2-0/#agent-operable-by-design" class="hash-link" aria-label="Пряме посилання на Agent-operable by design" title="Пряме посилання на Agent-operable by design" translate="no">​</a></h2>
<p>Durable Workflow 2.0 treats agent operability as a product contract. Human
operators and autonomous agents use the same machine-readable surfaces for
capability discovery, workflow commands, typed results, history, diagnostics,
and bounded repair operations.</p>
<p>Those contracts are available through the HTTP API, structured CLI output,
the SDK clients, published schemas, and MCP-enabled application surfaces. An
agent does not need to scrape a dashboard or infer success from log text. It
can discover what the runtime supports, make a scoped change, observe the
named result, diagnose a typed failure, apply an allowed repair, and verify the
new state.</p>
<p>The full <a class="" href="https://durable-workflow.com/uk/docs/ai-agent-workflow-engine/">agent operating contract</a>
documents that Discover, Change, Run, Diagnose, Repair loop.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="recovery-evidence-with-a-defined-boundary">Recovery evidence, with a defined boundary<a href="https://durable-workflow.com/uk/blog/durable-workflow-2-0/#recovery-evidence-with-a-defined-boundary" class="hash-link" aria-label="Пряме посилання на Recovery evidence, with a defined boundary" title="Пряме посилання на Recovery evidence, with a defined boundary" translate="no">​</a></h2>
<p>Durable Workflow 2.0 has been exercised against API node loss, database
interruption, Redis interruption, worker loss and replacement, worker restart,
server restart while timers are pending, and scheduler restart. The measured
scenarios preserved durable state, recovered execution, reached one terminal
workflow outcome, and refused duplicate completion attempts where applicable.</p>
<p>Those are exact claims for the tested single-region topology. They are not a
promise of universal exactly-once external side effects, arbitrary
network-partition safety, clock-skew tolerance, multi-region failover, or an
unbounded throughput level. Applications still make external activity effects
idempotent, and operators qualify the topology they intend to run.</p>
<p>The <a class="" href="https://durable-workflow.com/uk/docs/operator-operating-envelope/">operator operating envelope</a>
publishes what has been proven and what remains outside the 2.0 evidence
boundary. That specificity matters more than a blanket resilience claim.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="a-familiar-name-a-broader-platform">A familiar name, a broader platform<a href="https://durable-workflow.com/uk/blog/durable-workflow-2-0/#a-familiar-name-a-broader-platform" class="hash-link" aria-label="Пряме посилання на A familiar name, a broader platform" title="Пряме посилання на A familiar name, a broader platform" translate="no">​</a></h2>
<p>Durable Workflow 2.0 is for teams that build across languages, need long-running
work to survive process failure, and want operations to be equally legible to
people and software agents. It preserves the Laravel-native experience that
started the project while adding the standalone and managed runtime boundaries
needed by a polyglot system.</p>
<p>Start with the <a class="" href="https://durable-workflow.com/uk/docs/quickstart/">Durable Workflow 2.0 quickstart</a>, choose
<a class="" href="https://durable-workflow.com/uk/docs/polyglot/cloud-control-plane/">Cloud</a>,
<a class="" href="https://durable-workflow.com/uk/docs/polyglot/server/">Standalone Server</a>, or
<a class="" href="https://durable-workflow.com/uk/docs/introduction/">embedded Laravel</a>, and run your first durable workflow.</p>]]></content>
        <author>
            <name>Richard</name>
            <uri>https://github.com/rmcdaniel</uri>
        </author>
        <category label="durable-execution" term="durable-execution"/>
        <category label="polyglot" term="polyglot"/>
        <category label="agents" term="agents"/>
        <category label="php" term="php"/>
        <category label="python" term="python"/>
        <category label="rust" term="rust"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Learning Durable Workflow from the Sample App]]></title>
        <id>https://durable-workflow.com/uk/blog/learning-durable-workflow-from-the-sample-app/</id>
        <link href="https://durable-workflow.com/uk/blog/learning-durable-workflow-from-the-sample-app/"/>
        <updated>2026-05-08T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Most "how do I learn this thing?" answers are link dumps. A]]></summary>
        <content type="html"><![CDATA[<p>Most "how do I learn this thing?" answers are link dumps. A
quickstart, a feature tour, a couple of API references, maybe a video.
You read them and you can recognize the words but you can't yet write
the code, because nothing connects.</p>
<p>For Durable Workflow, there is one answer that connects: the
<a class="" href="https://durable-workflow.com/uk/docs/sample-app/">Sample App</a>. It is a runnable Laravel 13
project with one workflow per pattern surface — deterministic chains,
elapsed-time measurement, microservice coordination, browser
automation, webhook-started workflows, AI activity loops, and a
signal-driven travel-agent saga. Each one ships with an artisan
command that runs it, an MCP entry that exposes it to AI clients, and
a Waterline screen that proves the run committed.</p>
<p>This post walks through the loop the sample app is built around:
<strong>read, run, change</strong>. It is the loop we use ourselves when a new
engineer joins the project. Forty-five minutes later they can
explain the difference between a signal and an update, and they have
a workflow they wrote running in Waterline.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="read-the-sample">Read the sample<a href="https://durable-workflow.com/uk/blog/learning-durable-workflow-from-the-sample-app/#read-the-sample" class="hash-link" aria-label="Пряме посилання на Read the sample" title="Пряме посилання на Read the sample" translate="no">​</a></h2>
<p>The sample app's
<a href="https://github.com/durable-workflow/sample-app#sample-index" target="_blank" rel="noopener noreferrer" class="">README</a>
opens with a sample index — one row per pattern, naming the workflow
class, the artisan command, and the MCP key. That table is the only
map you need; every other piece of the sample app reads from it.</p>
<p>Open
<a href="https://github.com/durable-workflow/sample-app/blob/main/app/Workflows/Simple/SimpleWorkflow.php" target="_blank" rel="noopener noreferrer" class=""><code>App\Workflows\Simple\SimpleWorkflow</code></a>
first. It is the smallest possible v2 workflow: one activity in, one
activity out, one return value. Reading this class is how you learn
the file shape — <code>extends Workflow</code>, the <code>handle()</code> method, the use
of the <code>activity()</code> function instead of dispatching jobs. After this
class, every other sample reads as the same shape with one new piece.</p>
<p>If you are coming for a specific pattern, jump straight to it:</p>
<ul>
<li class=""><strong>Saga compensation under failure?</strong> Read
<a href="https://github.com/durable-workflow/sample-app/blob/main/app/Workflows/Ai/AiWorkflow.php" target="_blank" rel="noopener noreferrer" class=""><code>App\Workflows\Ai\AiWorkflow</code></a>.
It is a real travel-agent loop with hotel, flight, and rental
bookings; if any leg fails, the registered compensations unwind the
earlier ones in reverse order.</li>
<li class=""><strong>A workflow that parks until an external event?</strong> Read
<a href="https://github.com/durable-workflow/sample-app/blob/main/app/Workflows/Webhooks/WebhookWorkflow.php" target="_blank" rel="noopener noreferrer" class=""><code>App\Workflows\Webhooks\WebhookWorkflow</code></a>.
It starts from a webhook and waits on <code>await('ready')</code> until a
signal lands.</li>
<li class=""><strong>Elapsed time without replay drift?</strong> Read
<a href="https://github.com/durable-workflow/sample-app/blob/main/app/Workflows/Elapsed/ElapsedTimeWorkflow.php" target="_blank" rel="noopener noreferrer" class=""><code>App\Workflows\Elapsed\ElapsedTimeWorkflow</code></a>.
Every clock read is wrapped in <code>sideEffect()</code> and stored as an
integer timestamp.</li>
</ul>
<p>The point of reading first is that the sample app is not a tutorial
where steps land in order; it is a reference where each workflow is
independent and self-explanatory. You read the one you need.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="run-the-sample">Run the sample<a href="https://durable-workflow.com/uk/blog/learning-durable-workflow-from-the-sample-app/#run-the-sample" class="hash-link" aria-label="Пряме посилання на Run the sample" title="Пряме посилання на Run the sample" translate="no">​</a></h2>
<p>Reading is necessary but not sufficient. Until you watch a run land
in Waterline, "durable" is an abstract claim.</p>
<p>Spin up the codespace or run <code>docker compose up -d --build --wait app worker</code> locally. Then, in two terminals:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain"># terminal 1</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">php artisan queue:work</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"># terminal 2</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">php artisan app:workflow</span><br></div></code></pre></div></div>
<p>Open <code>http://localhost:8000/waterline/dashboard</code>. There is your run.
Click into it and you see the typed history events the workflow class
produced — <code>ActivityTaskScheduled</code>, <code>ActivityTaskCompleted</code>,
<code>WorkflowExecutionCompleted</code>. That list is the on-disk shape of the
class you just read.</p>
<p>Now run a more interesting one:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">php artisan app:webhook</span><br></div></code></pre></div></div>
<p>The workflow starts and parks on <code>await('ready')</code>. Waterline shows
the run in <code>Waiting</code> state. From a third terminal, send the signal
the workflow is waiting for, then refresh Waterline — the run
advances to <code>Completed</code>, and you can see the <code>WorkflowExecutionSignaled</code>
event in the timeline. That is what "signal" means in practice. You
just learned it from a five-second observation, not a paragraph.</p>
<p>Repeat the loop with <code>app:elapsed</code>, <code>app:microservice</code>, and <code>app:ai</code>
(for the last one, set <code>OPENAI_API_KEY</code> first; the workflow class
will fail fast if you forget). Each one teaches a different surface,
and each one is forty seconds of clicking around in Waterline after
the run completes.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="change-the-sample">Change the sample<a href="https://durable-workflow.com/uk/blog/learning-durable-workflow-from-the-sample-app/#change-the-sample" class="hash-link" aria-label="Пряме посилання на Change the sample" title="Пряме посилання на Change the sample" translate="no">​</a></h2>
<p>The third part of the loop is where the patterns become yours.</p>
<p>Pick a sample workflow you ran. Make a one-line change. Add a
<code>Log::info(...)</code> inside an activity. Increase a timer. Replace one
activity call with two parallel ones using <code>all([...])</code>. Run the
artisan command again, watch Waterline, and see how the typed history
differs.</p>
<p>This is when you start to feel which calls produce history events
and which calls do not. It is also when you find out which calls
break replay if you mis-place them — the
<a class="" href="https://durable-workflow.com/uk/docs/constraints/overview/">Constraints</a> page describes those
rules abstractly, but you understand them in your bones the first
time you accidentally call <code>now()</code> in workflow code and watch the
replay diverge.</p>
<p>When you are ready to build something new, follow the
<a class="" href="https://durable-workflow.com/uk/docs/contribute-a-sample/">Contribute a Sample</a> guide. The
guide describes the contract every merged sample meets: a workflow
class under <code>app/Workflows/&lt;Pattern&gt;/</code>, an artisan command, a
<code>config/workflow_mcp.php</code> entry, a test, a README index row, a
docs-site gallery row, and a cross-link from the matching pattern
page. If your idea passes the contract, it lands in the sample app
on a predictable cadence and the next engineer learns from it the
same way you just did.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-the-sample-app-and-not-a-tutorial">Why the sample app, and not a tutorial?<a href="https://durable-workflow.com/uk/blog/learning-durable-workflow-from-the-sample-app/#why-the-sample-app-and-not-a-tutorial" class="hash-link" aria-label="Пряме посилання на Why the sample app, and not a tutorial?" title="Пряме посилання на Why the sample app, and not a tutorial?" translate="no">​</a></h2>
<p>A tutorial decays. The minute the workflow package adds a new
attribute, a new function, or a new history event, the tutorial is
at risk of teaching yesterday's API. The sample app does not have
that problem because it is a <em>project</em>, not a document. CI runs
every sample on every push. The upstream-coverage manifest names
which features the sample app is expected to demonstrate, marks
each one <code>covered</code> or <code>gap</code>, and lints on every push. A feature
that ships upstream without a sample becomes visible in the
manifest, not in tribal memory. The
<a href="https://github.com/durable-workflow/workflow/blob/main/docs/sample-app/plan.md" target="_blank" rel="noopener noreferrer" class="">Sample-App Plan, Phase 4</a>
spells the cadence out: the pinned <code>durable-workflow/workflow</code>
version moves within one release cycle of every upstream tag.</p>
<p>That is also why this post links into the sample app instead of
inlining the workflow code. The class on the
<a href="https://github.com/durable-workflow/sample-app" target="_blank" rel="noopener noreferrer" class=""><code>main</code> branch</a>
is the version that just passed CI; the snippet I might paste into
a blog post would be a snapshot. Read the file in the repo, and you
will read the same code your local run is going to execute.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-to-read-next">What to read next<a href="https://durable-workflow.com/uk/blog/learning-durable-workflow-from-the-sample-app/#what-to-read-next" class="hash-link" aria-label="Пряме посилання на What to read next" title="Пряме посилання на What to read next" translate="no">​</a></h2>
<ul>
<li class=""><a class="" href="https://durable-workflow.com/uk/docs/sample-app/">Sample App</a> — the reference page, with the
full sample gallery and pattern-page cross-links.</li>
<li class=""><a class="" href="https://durable-workflow.com/uk/docs/contribute-a-sample/">Contribute a Sample</a> — the
contract for landing a new sample.</li>
<li class=""><a class="" href="https://durable-workflow.com/uk/docs/how-it-works/">How It Works</a> — the engine internals
story for when "the run committed" stops being magic and you want
to know how.</li>
</ul>
<p>The fastest way to learn Durable Workflow is to read a sample, run
it in Waterline, and change one line. Forty-five minutes; one loop.</p>]]></content>
        <author>
            <name>Richard</name>
            <uri>https://github.com/rmcdaniel</uri>
        </author>
        <category label="sample-app" term="sample-app"/>
        <category label="workflow" term="workflow"/>
        <category label="learning" term="learning"/>
        <category label="patterns" term="patterns"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Laravel AI Travel Agent with Saga Compensation]]></title>
        <id>https://durable-workflow.com/uk/blog/building-a-durable-ai-travel-agent-with-laravel/</id>
        <link href="https://durable-workflow.com/uk/blog/building-a-durable-ai-travel-agent-with-laravel/"/>
        <updated>2026-02-07T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Build a Laravel AI travel agent that keeps conversations durable and cancels earlier bookings with saga compensation when a later booking fails.]]></summary>
        <content type="html"><![CDATA[<p>A multi-booking AI conversation has a hard failure case: one reservation succeeds and the next fails. This tutorial combines the Laravel AI SDK with Durable Workflow so the conversation survives restarts and saga compensation cancels earlier bookings when a later booking fails.</p>
<p>The failure path is visible in these execution logs:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">BookHotelActivity ........... RUNNING</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">Booking hotel: Grand Hotel, Paris, 2026-03-01 to 2026-03-03, 1 guest(s). Confirmation #902928</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">BookHotelActivity .......... 4.35ms DONE</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">BookFlightActivity .......... RUNNING</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">BookFlightActivity ......... 8.37ms FAIL</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">CancelHotelActivity ......... RUNNING</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">Cancelling hotel Hotel booked: Grand Hotel, Paris, check-in 2026-03-01,</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">check-out 2026-03-03, 1 guest(s). Confirmation #902928...</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">CancelHotelActivity ....... 3.74ms DONE</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">AiWorkflow ............... 10.96ms DONE</span><br></div></code></pre></div></div>
<p>Read that again. A user asked to book a hotel and a flight in a single message. The hotel went through. The flight failed. And the system <em>automatically cancelled the hotel using the original confirmation number</em>. The user saw this:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">Agent: Flight booking failed: New York to Paris.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">       Any previous bookings have been cancelled.</span><br></div></code></pre></div></div>
<p>Distributed transaction management that just works.</p>
<p>This is a durable AI travel agent built with the Laravel AI SDK and Durable Workflow. It's one continuous conversation where every possible outcome is handled gracefully: successful bookings, partial failures, timeouts, and saga compensation, all in about 100 lines of workflow code.</p>
<p>Let's build it.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-problem">The Problem<a href="https://durable-workflow.com/uk/blog/building-a-durable-ai-travel-agent-with-laravel/#the-problem" class="hash-link" aria-label="Пряме посилання на The Problem" title="Пряме посилання на The Problem" translate="no">​</a></h2>
<p>Imagine you're tasked with building a conversational AI travel agent. Not a toy, a real one. Users chat with it, ask it to book hotels, flights, and rental cars, and expect it to handle failures gracefully.</p>
<p>Here's what you'd need with traditional Laravel patterns:</p>
<p><strong>State management.</strong> A <code>conversation_state</code> table tracking where each user is in the flow. A state machine (or a mess of <code>if</code> statements) to handle transitions. What happens if the user sends a message while a booking is in progress? What if two queue workers pick up the same conversation?</p>
<p><strong>Failure handling.</strong> An event listener on <code>BookingFailed</code>. Another listener to figure out which previous bookings need to be cancelled. A database query to look up confirmation numbers. A job to call each cancellation API. Another listener in case the <em>cancellation</em> fails.</p>
<p><strong>Timeouts.</strong> A cron job that runs every minute, queries for "stale" conversations, and closes them. Edge cases when a user sends a message at the exact moment the cron job fires.</p>
<p><strong>Cleanup.</strong> Scheduled commands to archive old conversations. Orphan detection for bookings that got confirmed but whose conversations crashed. Manual intervention scripts for the support team.</p>
<p>You're looking at 500+ lines of infrastructure code scattered across jobs, events, listeners, models, migrations, service classes, and cron configurations. And you haven't written a single line of business logic yet.</p>
<p>There's a better way.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-solution-laravel-ai-sdk--durable-workflows">The Solution: Laravel AI SDK + Durable Workflows<a href="https://durable-workflow.com/uk/blog/building-a-durable-ai-travel-agent-with-laravel/#the-solution-laravel-ai-sdk--durable-workflows" class="hash-link" aria-label="Пряме посилання на The Solution: Laravel AI SDK + Durable Workflows" title="Пряме посилання на The Solution: Laravel AI SDK + Durable Workflows" translate="no">​</a></h2>
<p>We're going to build this with three things:</p>
<ol>
<li class=""><strong>Laravel AI SDK</strong>, for the conversational agent and tool calling</li>
<li class=""><strong>Durable Workflow</strong>, for durable execution, saga compensation, and timeouts</li>
<li class=""><strong>About 100 lines of actual business logic</strong></li>
</ol>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-1-create-the-agent">Step 1: Create the Agent<a href="https://durable-workflow.com/uk/blog/building-a-durable-ai-travel-agent-with-laravel/#step-1-create-the-agent" class="hash-link" aria-label="Пряме посилання на Step 1: Create the Agent" title="Пряме посилання на Step 1: Create the Agent" translate="no">​</a></h3>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">php artisan make:agent TravelAgent</span><br></div></code></pre></div></div>
<p>And give it some tools:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">php artisan make:tool BookHotel</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">php artisan make:tool BookFlight</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">php artisan make:tool BookRentalCar</span><br></div></code></pre></div></div>
<p>The agent is straightforward:</p>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">class TravelAgent implements Agent, Conversational, HasTools</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    use Promptable;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    private array $messages = [];</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function instructions(): Stringable|string</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        return &lt;&lt;&lt;'INSTRUCTIONS'</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        You are a professional travel agent. Help users plan and book travel.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        BOOKING RULES:</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        - When a user asks to book a hotel, flight, or rental car, ALWAYS call</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">          the appropriate booking tool immediately with whatever details they</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">          provided. Never ask for more details before calling the tool.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        - Use reasonable defaults for any missing information (e.g. 1 guest,</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">          next-day dates, economy class).</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        - You may call multiple booking tools in a single response if the user</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">          requests multiple bookings.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        - For flights, always include a return date if the user mentions round</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">          trip, return dates, or trip end dates. Omit return_date only for</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">          explicitly one-way flights.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        CONVERSATION RULES:</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        - Be concise and action-oriented.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        - After placing bookings, briefly confirm what was booked.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        - You can also help with itinerary planning, destination advice,</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">          packing lists, and general travel logistics.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        INSTRUCTIONS;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function continue($messages): static</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $this-&gt;messages = $messages;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        return $this;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function messages(): iterable</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        return $this-&gt;messages;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function tools(): iterable</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        return [</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            new BookHotel(),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            new BookFlight(),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            new BookRentalCar(),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        ];</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<p>Notice we implement <code>Conversational</code> but we don't use <code>RemembersConversations</code>. The workflow history is our conversation store. We pass it in through the <code>continue()</code> method.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-2-the-tools-pattern">Step 2: The Tools Pattern<a href="https://durable-workflow.com/uk/blog/building-a-durable-ai-travel-agent-with-laravel/#step-2-the-tools-pattern" class="hash-link" aria-label="Пряме посилання на Step 2: The Tools Pattern" title="Пряме посилання на Step 2: The Tools Pattern" translate="no">​</a></h3>
<p>Here's the <code>BookHotel</code> tool:</p>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">class BookHotel implements Tool</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public static array $pending = [];</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function description(): Stringable|string</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        return 'Book a hotel for the user.';</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function handle(Request $request): Stringable|string</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        self::$pending[] = [</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'type' =&gt; 'book_hotel',</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'hotel_name' =&gt; $request['hotel_name'],</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'check_in_date' =&gt; $request['check_in_date'],</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'check_out_date' =&gt; $request['check_out_date'],</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'guests' =&gt; (int) $request['guests'],</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        ];</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        return 'Booking hotel: ' . $request['hotel_name'] . ' from '</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            . $request['check_in_date'] . ' to ' . $request['check_out_date']</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            . ' for ' . $request['guests'] . ' guest(s)';</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function schema(JsonSchema $schema): array</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        return [</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'hotel_name' =&gt; $schema-&gt;string()-&gt;required()-&gt;description('The name and location of the hotel to book'),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'check_in_date' =&gt; $schema-&gt;string()-&gt;required()-&gt;description('Check-in date (YYYY-MM-DD)'),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'check_out_date' =&gt; $schema-&gt;string()-&gt;required()-&gt;description('Check-out date (YYYY-MM-DD)'),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'guests' =&gt; $schema-&gt;integer()-&gt;required()-&gt;description('Number of guests'),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        ];</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<p>This is the key insight: <strong>the tool doesn't book anything</strong>. It collects structured data from the AI into a static <code>$pending</code> array and returns a confirmation message to the agent. The actual booking happens later, inside the workflow, as a durable activity.</p>
<p>Why? Because tool calls happen inside the AI activity. If we booked the hotel directly in the tool's <code>handle()</code> method, the workflow wouldn't know about it and couldn't compensate on failure. By collecting the requests and processing them in the workflow, every side effect is durable and reversible.</p>
<p><code>BookFlight</code> and <code>BookRentalCar</code> follow the same pattern.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-3-the-activity">Step 3: The Activity<a href="https://durable-workflow.com/uk/blog/building-a-durable-ai-travel-agent-with-laravel/#step-3-the-activity" class="hash-link" aria-label="Пряме посилання на Step 3: The Activity" title="Пряме посилання на Step 3: The Activity" translate="no">​</a></h3>
<p>The <code>TravelAgentActivity</code> bridges the AI agent and the workflow:</p>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">class TravelAgentActivity extends Activity</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function handle($messages)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        BookHotel::$pending = [];</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        BookFlight::$pending = [];</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        BookRentalCar::$pending = [];</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $history = array_slice($messages, 0, -1);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $currentUserMessage = end($messages);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $response = (new TravelAgent())</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            -&gt;continue($history)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            -&gt;prompt($currentUserMessage-&gt;content);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $bookings = array_merge(</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            BookHotel::$pending,</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            BookFlight::$pending,</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            BookRentalCar::$pending,</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        );</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        return json_encode([</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'text' =&gt; (string) $response,</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'bookings' =&gt; $bookings,</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        ]);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<p>It resets the pending arrays, passes the conversation history to the agent, prompts it with the latest user message, and returns both the AI's text response <em>and</em> any booking requests the tools collected. The workflow gets everything it needs in one shot.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-workflow-where-it-all-comes-together">The Workflow: Where It All Comes Together<a href="https://durable-workflow.com/uk/blog/building-a-durable-ai-travel-agent-with-laravel/#the-workflow-where-it-all-comes-together" class="hash-link" aria-label="Пряме посилання на The Workflow: Where It All Comes Together" title="Пряме посилання на The Workflow: Where It All Comes Together" translate="no">​</a></h2>
<p>Here's the complete workflow. (You can also view it on <a href="https://github.com/durable-workflow/sample-app/blob/Laravel-12/app/Workflows/Ai/AiWorkflow.php" target="_blank" rel="noopener noreferrer" class="">GitHub</a>.) Read it top to bottom. It's the entire orchestration layer:</p>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">class AiWorkflow extends Workflow</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    private const INACTIVITY_TIMEOUT = '2 minutes';</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    private const MAX_MESSAGES = 20;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    #[SignalMethod]</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function send(string $message): void</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $this-&gt;inbox-&gt;receive($message);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    #[UpdateMethod]</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function receive()</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        return $this-&gt;outbox-&gt;nextUnsent();</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function handle($injectFailure = null)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $messages = [];</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        try {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            while (count($messages) &lt; self::MAX_MESSAGES) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                $receivedMessage = yield awaitWithTimeout(</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                    self::INACTIVITY_TIMEOUT,</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                    fn () =&gt; $this-&gt;inbox-&gt;hasUnread(),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                );</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                if (! $receivedMessage) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                    throw new Exception(</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                        'Session ended due to inactivity. Please start a new conversation.'</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                    );</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                $messages[] = new UserMessage($this-&gt;inbox-&gt;nextUnread());</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                $result = yield activity(TravelAgentActivity::class, $messages);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                $data = json_decode($result, true);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                foreach ($data['bookings'] as $booking) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                    yield from $this-&gt;handleBooking($booking, $injectFailure);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                $messages[] = new AssistantMessage($data['text']);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                $this-&gt;outbox-&gt;send($data['text']);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            if (count($messages) &gt;= self::MAX_MESSAGES) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                throw new Exception(</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                    'This conversation has reached its message limit. '</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                    . 'Please start a new conversation to continue.'</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                );</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        } catch (Throwable $th) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            yield from $this-&gt;compensate();</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            $this-&gt;outbox-&gt;send(</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                $th-&gt;getMessage() . ' Any previous bookings have been cancelled.'</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            );</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        return $messages;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    private function handleBooking(array $data, ?string $injectFailure)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        return match ($data['type']) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'book_hotel' =&gt; $this-&gt;bookHotel($data, $injectFailure),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'book_flight' =&gt; $this-&gt;bookFlight($data, $injectFailure),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'book_rental_car' =&gt; $this-&gt;bookRentalCar($data, $injectFailure),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        };</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    private function bookHotel(array $data, ?string $injectFailure)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $hotel = yield activity(</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            BookHotelActivity::class,</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            $data['hotel_name'],</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            $data['check_in_date'],</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            $data['check_out_date'],</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            (int) $data['guests'],</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            $injectFailure === 'hotel',</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        );</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $this-&gt;addCompensation(fn () =&gt; activity(CancelHotelActivity::class, $hotel));</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        return $hotel;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    private function bookFlight(array $data, ?string $injectFailure)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $flight = yield activity(</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            BookFlightActivity::class,</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            $data['origin'],</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            $data['destination'],</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            $data['departure_date'],</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            $data['return_date'] ?? null,</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            $injectFailure === 'flight',</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        );</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $this-&gt;addCompensation(fn () =&gt; activity(CancelFlightActivity::class, $flight));</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        return $flight;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    private function bookRentalCar(array $data, ?string $injectFailure)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $rentalCar = yield activity(</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            BookRentalCarActivity::class,</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            $data['pickup_location'],</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            $data['pickup_date'],</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            $data['return_date'],</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            $injectFailure === 'car',</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        );</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $this-&gt;addCompensation(fn () =&gt; activity(CancelRentalCarActivity::class, $rentalCar));</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        return $rentalCar;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<p>There's a lot here. Let's break it down.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-inbox--outbox-pattern">The Inbox / Outbox Pattern<a href="https://durable-workflow.com/uk/blog/building-a-durable-ai-travel-agent-with-laravel/#the-inbox--outbox-pattern" class="hash-link" aria-label="Пряме посилання на The Inbox / Outbox Pattern" title="Пряме посилання на The Inbox / Outbox Pattern" translate="no">​</a></h3>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">#[SignalMethod]</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">public function send(string $message): void</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    $this-&gt;inbox-&gt;receive($message);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">#[UpdateMethod]</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">public function receive()</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    return $this-&gt;outbox-&gt;nextUnsent();</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<p>The workflow has two communication channels. The <strong>inbox</strong> receives user messages via <code>SignalMethod</code>, fire-and-forget signals that get appended to a durable queue. The <strong>outbox</strong> holds agent responses, retrieved via <code>UpdateMethod</code>, synchronous queries that replay the workflow and return the next unsent message.</p>
<p>This is durable messaging. If the server crashes between receiving a user message and processing it, the message is still in the inbox when the workflow resumes. If the agent produces a response but the client disconnects before reading it, it's still in the outbox on the next poll.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="timeout-as-business-logic">Timeout as Business Logic<a href="https://durable-workflow.com/uk/blog/building-a-durable-ai-travel-agent-with-laravel/#timeout-as-business-logic" class="hash-link" aria-label="Пряме посилання на Timeout as Business Logic" title="Пряме посилання на Timeout as Business Logic" translate="no">​</a></h3>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">$receivedMessage = yield awaitWithTimeout(</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    self::INACTIVITY_TIMEOUT,</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    fn () =&gt; $this-&gt;inbox-&gt;hasUnread(),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">if (! $receivedMessage) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    throw new Exception('Session ended due to inactivity. Please start a new conversation.');</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<p><code>awaitWithTimeout</code> pauses the workflow for up to 2 minutes, waiting for the condition to become true. If the user sends a message, execution continues immediately. If they don't, it returns <code>false</code> and we throw an exception to end the conversation.</p>
<p>No cron job. No scheduled command. No database polling. The timeout is expressed <em>as part of the business logic</em>, right where it belongs. The framework handles the timer durably. If the server restarts during the 2-minute window, the timer picks up where it left off.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-conversation-loop">The Conversation Loop<a href="https://durable-workflow.com/uk/blog/building-a-durable-ai-travel-agent-with-laravel/#the-conversation-loop" class="hash-link" aria-label="Пряме посилання на The Conversation Loop" title="Пряме посилання на The Conversation Loop" translate="no">​</a></h3>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">while (count($messages) &lt; self::MAX_MESSAGES) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    // Wait for user input (with timeout)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    // Read the message</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    // Run the AI agent</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    // Process any bookings</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    // Send the response</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<p>This reads like pseudocode, but it's the real implementation. Each iteration:</p>
<ol>
<li class=""><strong>Waits</strong> for a user message (durably, with timeout)</li>
<li class=""><strong>Reads</strong> the next unread message from the inbox</li>
<li class=""><strong>Runs</strong> the AI agent as a durable activity, passing the full conversation history</li>
<li class=""><strong>Processes</strong> any booking requests the agent's tools collected</li>
<li class=""><strong>Sends</strong> the agent's text response to the outbox</li>
</ol>
<p>The <code>$messages</code> array accumulates <code>UserMessage</code> and <code>AssistantMessage</code> objects as the conversation progresses. It's passed to the agent on every turn so it has full context. And because everything is inside a durable workflow, if the queue worker crashes after step 3 but before step 5, it replays from where it left off.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-saga-pattern-star-of-the-show">The Saga Pattern: Star of the Show<a href="https://durable-workflow.com/uk/blog/building-a-durable-ai-travel-agent-with-laravel/#the-saga-pattern-star-of-the-show" class="hash-link" aria-label="Пряме посилання на The Saga Pattern: Star of the Show" title="Пряме посилання на The Saga Pattern: Star of the Show" translate="no">​</a></h3>
<p>This is where it gets interesting.</p>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">private function bookHotel(array $data, ?string $injectFailure)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    $hotel = yield activity(</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        BookHotelActivity::class,</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $data['hotel_name'],</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $data['check_in_date'],</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $data['check_out_date'],</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        (int) $data['guests'],</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $injectFailure === 'hotel',</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    );</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    $this-&gt;addCompensation(fn () =&gt; activity(CancelHotelActivity::class, $hotel));</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    return $hotel;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<p>After each successful booking, we register a compensation action. <code>addCompensation</code> takes a callable that knows <em>exactly</em> how to undo what was just done, including the confirmation number, dates, and all the details returned by the booking activity.</p>
<p>If any subsequent step throws an exception, the <code>catch</code> block runs:</p>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">catch (Throwable $th) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    yield from $this-&gt;compensate();</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    $this-&gt;outbox-&gt;send(</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $th-&gt;getMessage() . ' Any previous bookings have been cancelled.'</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    );</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<p><code>$this-&gt;compensate()</code> executes all registered compensation actions <strong>in reverse order</strong>. If you booked a hotel, then a flight, then a rental car, and the rental car fails, the flight gets cancelled first, then the hotel. (To cancel them in parallel instead, we can set <code>$this-&gt;setParallelCompensation(true)</code>.)</p>
<p>And notice: the inactivity timeout and message limit are thrown as exceptions too. If a user walks away mid-booking, the <code>catch</code> block fires, compensation runs, and all their reservations get cleaned up. Every exit path goes through the same cleanup logic.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-just-happened">What Just Happened<a href="https://durable-workflow.com/uk/blog/building-a-durable-ai-travel-agent-with-laravel/#what-just-happened" class="hash-link" aria-label="Пряме посилання на What Just Happened" title="Пряме посилання на What Just Happened" translate="no">​</a></h2>
<p>Let's trace through the actual execution when a user books a hotel and a flight, but the flight fails:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">You: book Grand Hotel in Paris for 2 guests, check-in 2026-03-01,</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">     check-out 2026-03-03. Also book a flight NYC to Paris departing</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">     2026-03-01 returning 2026-03-03.</span><br></div></code></pre></div></div>
<p><strong>1. Hotel books successfully</strong></p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">BookHotelActivity ........... RUNNING</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">Booking hotel: Grand Hotel, Paris, 2026-03-01 to 2026-03-03,</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">2 guest(s). Confirmation #902928</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">BookHotelActivity .......... 4.35ms DONE</span><br></div></code></pre></div></div>
<p>The workflow now has a compensation registered:</p>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">fn () =&gt; activity(CancelHotelActivity::class, "Hotel booked: Grand Hotel... Confirmation #902928")</span><br></div></code></pre></div></div>
<p><strong>2. Flight fails</strong> (injected failure for demo purposes)</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">BookFlightActivity .......... RUNNING</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">BookFlightActivity ......... 8.37ms FAIL</span><br></div></code></pre></div></div>
<p>The <code>NonRetryableException</code> propagates up to the <code>catch</code> block.</p>
<p><strong>3. Saga compensation kicks in automatically</strong></p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">CancelHotelActivity ......... RUNNING</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">Cancelling hotel Hotel booked: Grand Hotel, Paris, check-in 2026-03-01,</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">check-out 2026-03-03, 2 guest(s). Confirmation #902928...</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">CancelHotelActivity ....... 3.74ms DONE</span><br></div></code></pre></div></div>
<p>The framework ran the compensation with the <em>exact</em> confirmation details from the original booking.</p>
<p><strong>4. User gets clean feedback</strong></p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">Agent: Flight booking failed: New York to Paris.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">       Any previous bookings have been cancelled.</span><br></div></code></pre></div></div>
<p>The error message is conversational, not a stack trace. The user knows what happened and what was cleaned up.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="without-sagas">Without Sagas?<a href="https://durable-workflow.com/uk/blog/building-a-durable-ai-travel-agent-with-laravel/#without-sagas" class="hash-link" aria-label="Пряме посилання на Without Sagas?" title="Пряме посилання на Without Sagas?" translate="no">​</a></h2>
<p>Consider what you'd need without this pattern:</p>
<ul>
<li class=""><strong>Orphaned hotel booking.</strong> Confirmation #902928 is still reserved, costing real money.</li>
<li class=""><strong>Manual cleanup.</strong> Someone has to find and cancel it.</li>
<li class=""><strong>Database queries</strong> to figure out which bookings belong to this conversation.</li>
<li class=""><strong>Race conditions.</strong> What if the user retries while you're cleaning up?</li>
<li class=""><strong>Scattered compensation logic.</strong> Cancel handlers spread across event listeners, with no guarantee they all run.</li>
<li class=""><strong>Angry customers and support tickets.</strong> The inevitable result.</li>
</ul>
<p>With sagas, it's one line per booking:</p>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">$this-&gt;addCompensation(fn () =&gt; activity(CancelHotelActivity::class, $hotel));</span><br></div></code></pre></div></div>
<p>The framework handles the rest.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="key-innovations">Key Innovations<a href="https://durable-workflow.com/uk/blog/building-a-durable-ai-travel-agent-with-laravel/#key-innovations" class="hash-link" aria-label="Пряме посилання на Key Innovations" title="Пряме посилання на Key Innovations" translate="no">​</a></h2>
<p><strong>Timeouts as business logic.</strong> <code>awaitWithTimeout('2 minutes', ...)</code> expresses a timeout right in the workflow code, not as infrastructure configuration. If the user goes idle, the conversation ends gracefully with compensation.</p>
<p><strong>Conversational error messages.</strong> Every failure path (booking errors, timeouts, message limits) flows through the outbox as a normal message. The user never sees a stack trace or a "Something went wrong" page.</p>
<p><strong>Automatic cleanup on every exit.</strong> The <code>try/catch</code> wrapping the entire conversation loop means <em>any</em> exception triggers compensation. The conversation can't end with orphaned bookings, no matter how it ends.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-traditional-approach">The Traditional Approach<a href="https://durable-workflow.com/uk/blog/building-a-durable-ai-travel-agent-with-laravel/#the-traditional-approach" class="hash-link" aria-label="Пряме посилання на The Traditional Approach" title="Пряме посилання на The Traditional Approach" translate="no">​</a></h2>
<p>Let's estimate what this would take with traditional Laravel patterns:</p>
<table><thead><tr><th>Concern</th><th>Traditional</th><th>Durable Workflow</th></tr></thead><tbody><tr><td>State tracking</td><td>Database table + state machine</td><td>Implicit in workflow position</td></tr><tr><td>Timeout handling</td><td>Cron job + stale detection</td><td><code>awaitWithTimeout()</code></td></tr><tr><td>Failure compensation</td><td>Event listeners + manual queries</td><td><code>addCompensation()</code> + <code>compensate()</code></td></tr><tr><td>Crash recovery</td><td>Custom retry logic + idempotency</td><td>Automatic replay</td></tr><tr><td>Race conditions</td><td>Locks + transactions</td><td>Single-threaded workflow execution</td></tr><tr><td>Cleanup</td><td>Scheduled commands + orphan detection</td><td>Catch block</td></tr><tr><td><strong>Total</strong></td><td><strong>~500 lines across 10+ files</strong></td><td><strong>~100 lines in 1 file</strong></td></tr></tbody></table>
<p>The traditional approach isn't just more code, it's more <em>categories</em> of code. You're writing infrastructure: state machines, cleanup jobs, event wiring, retry logic. With a durable workflow, you're writing business logic: wait for a message, call the agent, book the hotel, compensate on failure.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="observability">Observability<a href="https://durable-workflow.com/uk/blog/building-a-durable-ai-travel-agent-with-laravel/#observability" class="hash-link" aria-label="Пряме посилання на Observability" title="Пряме посилання на Observability" translate="no">​</a></h2>
<p>Every message, every activity, every retry, every timeout, every exception, every compensation step, all of it is visible in real time in <a href="https://github.com/durable-workflow/waterline" target="_blank" rel="noopener noreferrer" class="">Waterline</a>.</p>
<p>You can literally scroll through the workflow and see:</p>
<ul>
<li class="">Each user message arriving via a signal</li>
<li class="">Each AI turn as a durable activity</li>
<li class="">Every booking attempt with inputs and outputs</li>
<li class="">The exact moment a failure occurs (and the line it occured on with a stack trace)</li>
<li class="">The saga compensation steps, executed automatically in reverse order</li>
<li class="">How long each step took, down to the millisecond</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="conclusion">Conclusion<a href="https://durable-workflow.com/uk/blog/building-a-durable-ai-travel-agent-with-laravel/#conclusion" class="hash-link" aria-label="Пряме посилання на Conclusion" title="Пряме посилання на Conclusion" translate="no">​</a></h2>
<p>This is a paradigm shift. Instead of building infrastructure to manage state, handle failures, and coordinate distributed operations, you write a function that describes what should happen. The framework provides durability, retry, compensation, and crash recovery.</p>
<p>The entire travel agent (AI conversation, multi-step bookings, saga compensation, inactivity timeouts, message limits, and graceful error handling) is expressed in a single workflow class. Production-grade UX with development-friendly code.</p>
<p>No state machine tables. No cleanup crons. No orphaned bookings. No scattered event listeners. Just a workflow that reads like the business requirements it implements.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="try-it-now-in-your-browser">Try It Now in Your Browser<a href="https://durable-workflow.com/uk/blog/building-a-durable-ai-travel-agent-with-laravel/#try-it-now-in-your-browser" class="hash-link" aria-label="Пряме посилання на Try It Now in Your Browser" title="Пряме посилання на Try It Now in Your Browser" translate="no">​</a></h3>
<p>We’ve bundled this workflow into the official Workflow <a href="https://github.com/durable-workflow/sample-app/tree/Laravel-12" target="_blank" rel="noopener noreferrer" class="">Sample App</a>.</p>
<p>To try it:</p>
<ol>
<li class="">Open the sample-app repo's <code>Laravel-12</code> branch on GitHub</li>
<li class="">Click <strong>Code</strong> → <strong>Codespaces</strong> → <strong>Create codespace on Laravel-12</strong></li>
<li class="">Wait for the environment to build</li>
<li class="">Set your OPENAI_API_KEY in the .env</li>
<li class="">Setup the app and start the queue worker:<!-- -->
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">php artisan app:init</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">php artisan queue:work</span><br></div></code></pre></div></div>
</li>
<li class="">In a second terminal:</li>
</ol>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">php artisan app:ai</span><br></div></code></pre></div></div>
<p>Note: You can optionally inject a failure at one of the booking steps by running it with the <code>--inject-failure</code> flag e.g. <code>php artisan app:ai --inject-failure flight</code>. Valid options are <code>hotel</code>, <code>flight</code> or <code>car</code>.</p>]]></content>
        <author>
            <name>Richard</name>
            <uri>https://github.com/rmcdaniel</uri>
        </author>
        <category label="ai" term="ai"/>
        <category label="workflow" term="workflow"/>
        <category label="agents" term="agents"/>
        <category label="tools" term="tools"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Laravel Workflows as MCP Tools for AI Clients]]></title>
        <id>https://durable-workflow.com/uk/blog/laravel-workflows-as-mcp-tools-for-ai-clients/</id>
        <link href="https://durable-workflow.com/uk/blog/laravel-workflows-as-mcp-tools-for-ai-clients/"/>
        <updated>2025-12-03T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Build a Laravel MCP server that lets AI clients discover, start, and monitor durable workflows asynchronously through three focused tools.]]></summary>
        <content type="html"><![CDATA[<p>AI clients need a non-blocking way to launch work that outlives one request. This tutorial builds a Laravel MCP server that exposes durable workflows as tools clients can discover, start asynchronously, and monitor through completion.</p>
<p>In this post, we'll show how to build an MCP server that allows AI clients to:</p>
<ul>
<li class="">Discover available workflows</li>
<li class="">Start workflows asynchronously</li>
<li class="">Poll for status and retrieve results</li>
</ul>
<p>This creates a powerful pattern where AI agents can orchestrate long-running, durable workflows, perfect for complex tasks that can't complete in a single request.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-mcp--durable-workflow">Why MCP + Durable Workflow?<a href="https://durable-workflow.com/uk/blog/laravel-workflows-as-mcp-tools-for-ai-clients/#why-mcp--durable-workflow" class="hash-link" aria-label="Пряме посилання на Why MCP + Durable Workflow?" title="Пряме посилання на Why MCP + Durable Workflow?" translate="no">​</a></h3>
<p>Durable Workflow (formerly Laravel Workflow) excels at durable, stateful execution. MCP excels at giving AI clients structured access to external capabilities. Together, they enable:</p>
<ul>
<li class=""><strong>Async AI operations</strong>: Start a workflow, continue the conversation, check results later</li>
<li class=""><strong>Reliable execution</strong>: Workflows survive crashes, retries, and long wait times</li>
<li class=""><strong>Observability</strong>: Track every workflow through Waterline's dashboard</li>
<li class=""><strong>Stateless servers</strong>: The MCP server doesn't hold state. Clients track workflow IDs</li>
</ul>
<p>This mirrors how humans delegate tasks: "Start this report, I'll check back later."</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-were-building">What We're Building<a href="https://durable-workflow.com/uk/blog/laravel-workflows-as-mcp-tools-for-ai-clients/#what-were-building" class="hash-link" aria-label="Пряме посилання на What We're Building" title="Пряме посилання на What We're Building" translate="no">​</a></h3>
<p>We'll create an MCP server with three tools:</p>
<table><thead><tr><th>Tool</th><th>Purpose</th></tr></thead><tbody><tr><td><code>list_workflows</code></td><td>Discover available workflows and view recent runs</td></tr><tr><td><code>start_workflow</code></td><td>Start a workflow and get a tracking ID</td></tr><tr><td><code>get_workflow_result</code></td><td>Check status and retrieve output when complete</td></tr></tbody></table>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-by-step-implementation">Step-by-Step Implementation<a href="https://durable-workflow.com/uk/blog/laravel-workflows-as-mcp-tools-for-ai-clients/#step-by-step-implementation" class="hash-link" aria-label="Пряме посилання на Step-by-Step Implementation" title="Пряме посилання на Step-by-Step Implementation" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-install-laravel-mcp">1. Install Laravel MCP<a href="https://durable-workflow.com/uk/blog/laravel-workflows-as-mcp-tools-for-ai-clients/#1-install-laravel-mcp" class="hash-link" aria-label="Пряме посилання на 1. Install Laravel MCP" title="Пряме посилання на 1. Install Laravel MCP" translate="no">​</a></h4>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">composer require laravel/mcp</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">php artisan vendor:publish --tag=ai-routes</span><br></div></code></pre></div></div>
<p>This gives you <code>routes/ai.php</code> where you'll register your MCP server.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-create-the-mcp-server">2. Create the MCP Server<a href="https://durable-workflow.com/uk/blog/laravel-workflows-as-mcp-tools-for-ai-clients/#2-create-the-mcp-server" class="hash-link" aria-label="Пряме посилання на 2. Create the MCP Server" title="Пряме посилання на 2. Create the MCP Server" translate="no">​</a></h4>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">php artisan make:mcp-server WorkflowServer</span><br></div></code></pre></div></div>
<p>Configure it with instructions for the AI:</p>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">namespace App\Mcp\Servers;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use App\Mcp\Tools\GetWorkflowResultTool;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use App\Mcp\Tools\ListWorkflowsTool;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use App\Mcp\Tools\StartWorkflowTool;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Laravel\Mcp\Server;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">class WorkflowServer extends Server</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    protected string $name = 'Workflow Server';</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    protected string $version = '1.0.0';</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    protected string $instructions = &lt;&lt;&lt;'MARKDOWN'</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        This server allows you to start and monitor Workflows.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        ## Typical Usage Pattern</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        1. Call `list_workflows` to see what workflows are available.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        2. Call `start_workflow` with the workflow name and arguments.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        3. Store the returned `workflow_id`.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        4. Call `get_workflow_result` until status is `WorkflowCompletedStatus`.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        5. Read the `output` field for the result.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        ## Status Values</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        - `WorkflowCreatedStatus` - Workflow has been created</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        - `WorkflowPendingStatus` - Queued for execution</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        - `WorkflowRunningStatus` - Currently executing</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        - `WorkflowWaitingStatus` - Waiting (timer, signal, etc.)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        - `WorkflowCompletedStatus` - Finished successfully</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        - `WorkflowFailedStatus` - Encountered an error</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    MARKDOWN;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    protected array $tools = [</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        ListWorkflowsTool::class,</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        StartWorkflowTool::class,</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        GetWorkflowResultTool::class,</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    ];</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="3-create-the-start-workflow-tool">3. Create the Start Workflow Tool<a href="https://durable-workflow.com/uk/blog/laravel-workflows-as-mcp-tools-for-ai-clients/#3-create-the-start-workflow-tool" class="hash-link" aria-label="Пряме посилання на 3. Create the Start Workflow Tool" title="Пряме посилання на 3. Create the Start Workflow Tool" translate="no">​</a></h4>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">php artisan make:mcp-tool StartWorkflowTool</span><br></div></code></pre></div></div>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">namespace App\Mcp\Tools;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Illuminate\Contracts\JsonSchema\JsonSchema;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Illuminate\Support\Arr;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Laravel\Mcp\Request;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Laravel\Mcp\Response;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Laravel\Mcp\Server\Tool;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Workflow\Workflow;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Workflow\WorkflowStub;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">class StartWorkflowTool extends Tool</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    protected string $description = &lt;&lt;&lt;'MARKDOWN'</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        Start a Workflow asynchronously and return its workflow ID.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        The workflow will execute in the background on the queue. Use the</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        `get_workflow_result` tool to poll for status and retrieve results</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        once the workflow completes.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    MARKDOWN;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function handle(Request $request): Response</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $data = $request-&gt;validate([</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'workflow' =&gt; ['required', 'string'],</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'args' =&gt; ['nullable', 'array'],</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'external_id' =&gt; ['nullable', 'string', 'max:255'],</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        ]);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $workflowKey = $data['workflow'];</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $args = Arr::get($data, 'args', []);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $externalId = $data['external_id'] ?? null;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $workflowClass = $this-&gt;resolveWorkflowClass($workflowKey);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        if ($workflowClass === null) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            return Response::error("Unknown workflow: {$workflowKey}");</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        if (! class_exists($workflowClass) || ! is_subclass_of($workflowClass, Workflow::class)) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            return Response::error("Invalid workflow class: {$workflowClass}");</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $stub = WorkflowStub::make($workflowClass);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $stub-&gt;start(...array_values($args));</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $status = $stub-&gt;status();</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $statusName = is_object($status) ? class_basename($status) : class_basename((string) $status);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        return Response::json([</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'workflow_id' =&gt; $stub-&gt;id(),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'workflow' =&gt; $workflowKey,</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'status' =&gt; $statusName,</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'external_id' =&gt; $externalId,</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'message' =&gt; 'Workflow started. Use get_workflow_result to poll status.',</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        ]);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    protected function resolveWorkflowClass(string $key): ?string</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $mapped = config("workflow_mcp.workflows.{$key}");</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        if ($mapped !== null) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            return $mapped;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        if (config('workflow_mcp.allow_fqcn', false) &amp;&amp; str_contains($key, '\\')) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            return $key;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        return null;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function schema(JsonSchema $schema): array</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $workflows = implode(', ', array_keys(config('workflow_mcp.workflows', [])));</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        return [</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'workflow' =&gt; $schema-&gt;string()</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                -&gt;description("The workflow to start. Available: {$workflows}"),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'args' =&gt; $schema-&gt;object()</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                -&gt;description('Arguments for the workflow execute() method.'),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'external_id' =&gt; $schema-&gt;string()</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                -&gt;description('Optional idempotency key for tracking.'),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        ];</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="4-create-the-get-result-tool">4. Create the Get Result Tool<a href="https://durable-workflow.com/uk/blog/laravel-workflows-as-mcp-tools-for-ai-clients/#4-create-the-get-result-tool" class="hash-link" aria-label="Пряме посилання на 4. Create the Get Result Tool" title="Пряме посилання на 4. Create the Get Result Tool" translate="no">​</a></h4>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">php artisan make:mcp-tool GetWorkflowResultTool</span><br></div></code></pre></div></div>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">namespace App\Mcp\Tools;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use App\Models\StoredWorkflow;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Illuminate\Contracts\JsonSchema\JsonSchema;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Laravel\Mcp\Request;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Laravel\Mcp\Response;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Laravel\Mcp\Server\Tool;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Workflow\States\WorkflowCompletedStatus;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Workflow\States\WorkflowFailedStatus;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Workflow\WorkflowStub;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">class GetWorkflowResultTool extends Tool</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    protected string $description = &lt;&lt;&lt;'MARKDOWN'</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        Fetch the status and, if completed, the output of a Workflow.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        Use the workflow_id returned by `start_workflow` to check progress.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        Once status is `WorkflowCompletedStatus`, the output field contains the result.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    MARKDOWN;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function handle(Request $request): Response</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $data = $request-&gt;validate([</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'workflow_id' =&gt; ['required'],</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        ]);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $workflowId = $data['workflow_id'];</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $stored = StoredWorkflow::find($workflowId);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        if (! $stored) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            return Response::json([</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                'found' =&gt; false,</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                'message' =&gt; "Workflow {$workflowId} not found.",</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            ]);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $workflow = WorkflowStub::load($workflowId);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $status = $workflow-&gt;status();</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $statusName = is_object($status) ? class_basename($status) : class_basename((string) $status);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $running = $workflow-&gt;running();</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $result = null;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $error = null;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        if (! $running &amp;&amp; str_contains($statusName, 'Completed')) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            $result = $workflow-&gt;output();</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        if (! $running &amp;&amp; str_contains($statusName, 'Failed')) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            $exception = $stored-&gt;exceptions()-&gt;latest()-&gt;first();</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            $error = $exception?-&gt;exception ?? 'Unknown error';</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        return Response::json([</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'found' =&gt; true,</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'workflow_id' =&gt; $workflowId,</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'status' =&gt; $statusName,</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'running' =&gt; $running,</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'output' =&gt; $result,</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'error' =&gt; $error,</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'created_at' =&gt; $stored-&gt;created_at?-&gt;toIso8601String(),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'updated_at' =&gt; $stored-&gt;updated_at?-&gt;toIso8601String(),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        ]);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function schema(JsonSchema $schema): array</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        return [</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'workflow_id' =&gt; $schema-&gt;string()</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                -&gt;description('The workflow ID returned by start_workflow.'),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        ];</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="5-create-the-list-workflows-tool">5. Create the List Workflows Tool<a href="https://durable-workflow.com/uk/blog/laravel-workflows-as-mcp-tools-for-ai-clients/#5-create-the-list-workflows-tool" class="hash-link" aria-label="Пряме посилання на 5. Create the List Workflows Tool" title="Пряме посилання на 5. Create the List Workflows Tool" translate="no">​</a></h4>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">php artisan make:mcp-tool ListWorkflowsTool</span><br></div></code></pre></div></div>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">namespace App\Mcp\Tools;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use App\Models\StoredWorkflow;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Illuminate\Contracts\JsonSchema\JsonSchema;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Laravel\Mcp\Request;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Laravel\Mcp\Response;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Laravel\Mcp\Server\Tool;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">class ListWorkflowsTool extends Tool</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    protected string $description = &lt;&lt;&lt;'MARKDOWN'</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        List available workflow types and optionally show recent workflow runs.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        Use this to discover what workflows can be started, or to see</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        the status of recent executions.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    MARKDOWN;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function handle(Request $request): Response</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $data = $request-&gt;validate([</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'show_recent' =&gt; ['nullable', 'boolean'],</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'limit' =&gt; ['nullable', 'integer', 'min:1', 'max:50'],</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'status' =&gt; ['nullable', 'string'],</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        ]);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $showRecent = $data['show_recent'] ?? false;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $limit = $data['limit'] ?? 10;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $statusFilter = $data['status'] ?? null;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $availableWorkflows = [];</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        foreach (config('workflow_mcp.workflows', []) as $key =&gt; $class) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            $availableWorkflows[] = ['key' =&gt; $key, 'class' =&gt; $class];</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $response = [</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'available_workflows' =&gt; $availableWorkflows,</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        ];</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        if ($showRecent) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            $query = StoredWorkflow::query()</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                -&gt;orderBy('created_at', 'desc')</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                -&gt;limit($limit);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            if ($statusFilter) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                $query-&gt;where('status', 'like', "%{$statusFilter}%");</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            $response['recent_workflows'] = $query-&gt;get()-&gt;map(function ($w) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                $status = $w-&gt;status;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                $statusName = is_object($status) ? class_basename($status) : class_basename((string) $status);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                return [</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                    'id' =&gt; $w-&gt;id,</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                    'class' =&gt; $w-&gt;class,</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                    'status' =&gt; $statusName,</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                    'created_at' =&gt; $w-&gt;created_at?-&gt;toIso8601String(),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                ];</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            });</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        return Response::json($response);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function schema(JsonSchema $schema): array</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        return [</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'show_recent' =&gt; $schema-&gt;boolean()</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                -&gt;description('Include recent workflow runs in response.'),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'limit' =&gt; $schema-&gt;integer()</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                -&gt;description('Max recent workflows to return (default: 10).'),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'status' =&gt; $schema-&gt;string()</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                -&gt;description('Filter by status (e.g., "Completed", "Failed").'),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        ];</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="6-configure-available-workflows">6. Configure Available Workflows<a href="https://durable-workflow.com/uk/blog/laravel-workflows-as-mcp-tools-for-ai-clients/#6-configure-available-workflows" class="hash-link" aria-label="Пряме посилання на 6. Configure Available Workflows" title="Пряме посилання на 6. Configure Available Workflows" translate="no">​</a></h4>
<p>Create <code>config/workflow_mcp.php</code> to whitelist which workflows AI clients can start:</p>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">return [</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    'allow_fqcn' =&gt; env('WORKFLOW_MCP_ALLOW_FQCN', false),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    'workflows' =&gt; [</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        'simple' =&gt; App\Workflows\Simple\SimpleWorkflow::class,</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        'prism' =&gt; App\Workflows\Prism\PrismWorkflow::class,</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    ],</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">];</span><br></div></code></pre></div></div>
<p>This prevents arbitrary class execution. Only mapped workflows are accessible.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="7-register-the-mcp-server">7. Register the MCP Server<a href="https://durable-workflow.com/uk/blog/laravel-workflows-as-mcp-tools-for-ai-clients/#7-register-the-mcp-server" class="hash-link" aria-label="Пряме посилання на 7. Register the MCP Server" title="Пряме посилання на 7. Register the MCP Server" translate="no">​</a></h4>
<p>Update <code>routes/ai.php</code>:</p>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">use App\Mcp\Servers\WorkflowServer;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Laravel\Mcp\Facades\Mcp;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">Mcp::web('/mcp/workflows', WorkflowServer::class);</span><br></div></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="connecting-ai-clients">Connecting AI Clients<a href="https://durable-workflow.com/uk/blog/laravel-workflows-as-mcp-tools-for-ai-clients/#connecting-ai-clients" class="hash-link" aria-label="Пряме посилання на Connecting AI Clients" title="Пряме посилання на Connecting AI Clients" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="vs-code--github-copilot">VS Code / GitHub Copilot<a href="https://durable-workflow.com/uk/blog/laravel-workflows-as-mcp-tools-for-ai-clients/#vs-code--github-copilot" class="hash-link" aria-label="Пряме посилання на VS Code / GitHub Copilot" title="Пряме посилання на VS Code / GitHub Copilot" translate="no">​</a></h4>
<p>Create <code>.vscode/mcp.json</code> in your project:</p>
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token property">"servers"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token property">"laravel-workflow"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token property">"type"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"http"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token property">"url"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"http://localhost/mcp/workflows"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><br></div></code></pre></div></div>
<p>This configuration works for both local development and GitHub Codespaces. In Codespaces, the VS Code server runs inside the container, so <code>localhost</code> correctly reaches the Laravel server without needing public ports or the <code>*.app.github.dev</code> URL.</p>
<p>After reloading VS Code (Cmd/Ctrl+Shift+P → "Developer: Reload Window"), Copilot can use the workflow tools directly in chat.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="real-world-usage">Real-World Usage<a href="https://durable-workflow.com/uk/blog/laravel-workflows-as-mcp-tools-for-ai-clients/#real-world-usage" class="hash-link" aria-label="Пряме посилання на Real-World Usage" title="Пряме посилання на Real-World Usage" translate="no">​</a></h3>
<p>Once connected, you can have natural conversations with your AI assistant:</p>
<blockquote>
<p><strong>You:</strong> "What workflows are available?"</p>
<p><strong>AI:</strong> <em>calls list_workflows</em> "I found 2 workflows: <code>simple</code> and <code>prism</code>."</p>
</blockquote>
<blockquote>
<p><strong>You:</strong> "Start the prism workflow"</p>
<p><strong>AI:</strong> <em>calls start_workflow</em> "Started workflow ID 42. I'll check its status."</p>
</blockquote>
<blockquote>
<p><strong>AI:</strong> <em>calls get_workflow_result</em> "The workflow completed! Here's the generated user profile: { name: 'Elena', hobbies: [...] }"</p>
</blockquote>
<p>This creates a seamless experience where AI assistants can orchestrate complex, long-running operations while keeping the user informed.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-makes-this-pattern-powerful">What Makes This Pattern Powerful<a href="https://durable-workflow.com/uk/blog/laravel-workflows-as-mcp-tools-for-ai-clients/#what-makes-this-pattern-powerful" class="hash-link" aria-label="Пряме посилання на What Makes This Pattern Powerful" title="Пряме посилання на What Makes This Pattern Powerful" translate="no">​</a></h3>
<ul>
<li class=""><strong>Durability</strong>: Workflows survive server restarts and network failures</li>
<li class=""><strong>Async by design</strong>: AI clients don't block waiting for completion</li>
<li class=""><strong>Observable</strong>: Every workflow is tracked in Waterline's dashboard</li>
<li class=""><strong>Secure</strong>: Whitelist-based workflow access prevents arbitrary execution</li>
<li class=""><strong>Stateless MCP</strong>: The server holds no state. Clients track workflow IDs</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="try-it-now-in-your-browser">Try It Now in Your Browser<a href="https://durable-workflow.com/uk/blog/laravel-workflows-as-mcp-tools-for-ai-clients/#try-it-now-in-your-browser" class="hash-link" aria-label="Пряме посилання на Try It Now in Your Browser" title="Пряме посилання на Try It Now in Your Browser" translate="no">​</a></h3>
<p>This MCP integration is included and pre-configured in the Durable Workflow <a href="https://github.com/durable-workflow/sample-app/tree/Laravel-12" target="_blank" rel="noopener noreferrer" class="">Sample App</a>.</p>
<p>To try it:</p>
<ol>
<li class="">Open the sample-app repo's <code>Laravel-12</code> branch on GitHub</li>
<li class="">Click <strong>Code</strong> → <strong>Codespaces</strong> → <strong>Create codespace on Laravel-12</strong></li>
<li class="">Wait for the environment to build</li>
<li class="">Setup the app and start the queue worker:<!-- -->
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">php artisan app:init</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">php artisan queue:work</span><br></div></code></pre></div></div>
</li>
<li class="">Enable the Durable Workflow Server MCP tools</li>
<li class="">Ask your AI to list and run workflows!</li>
</ol>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="where-to-go-from-here">Where to Go From Here<a href="https://durable-workflow.com/uk/blog/laravel-workflows-as-mcp-tools-for-ai-clients/#where-to-go-from-here" class="hash-link" aria-label="Пряме посилання на Where to Go From Here" title="Пряме посилання на Where to Go From Here" translate="no">​</a></h3>
<p>You can extend this pattern to:</p>
<ul>
<li class=""><strong>Parameterized workflows</strong>: Pass user input to workflow arguments</li>
<li class=""><strong>Webhook notifications</strong>: Push completion events instead of polling</li>
<li class=""><strong>Workflow signals</strong>: Let AI clients send signals to waiting workflows</li>
<li class=""><strong>Progress streaming</strong>: Use SSE to stream workflow progress in real-time</li>
<li class=""><strong>Multi-step agents</strong>: Chain multiple workflows together in a conversation</li>
</ul>
<p>The combination of Durable Workflow's durable execution and MCP's tool protocol creates a foundation for truly capable AI agents that can handle real-world complexity.</p>]]></content>
        <author>
            <name>Richard</name>
            <uri>https://github.com/rmcdaniel</uri>
        </author>
        <category label="ai" term="ai"/>
        <category label="workflow" term="workflow"/>
        <category label="mcp" term="mcp"/>
        <category label="agents" term="agents"/>
        <category label="tools" term="tools"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Building Reliable Agentic Loops with Workflow and PrismPHP]]></title>
        <id>https://durable-workflow.com/uk/blog/building-reliable-agentic-loops-with-laravel-workflow-and-prismphp/</id>
        <link href="https://durable-workflow.com/uk/blog/building-reliable-agentic-loops-with-laravel-workflow-and-prismphp/"/>
        <updated>2025-07-10T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[captionless image]]></summary>
        <content type="html"><![CDATA[<p><img decoding="async" loading="lazy" src="https://raw.githubusercontent.com/prism-php/prism/main/assets/prism-logo.webp" alt="captionless image" class="img_ev3q"></p>
<p>Workflow is a powerful tool for orchestrating long-running, stateful workflows in PHP. Paired with <a href="https://prismphp.com/" target="_blank" rel="noopener noreferrer" class="">PrismPHP</a>, it becomes a compelling foundation for building reliable AI agents that not only generate structured data but verify and retry until results meet strict real-world constraints.</p>
<p>In this post, we’ll show how to use Workflow + Prism to create an agentic loop that:</p>
<ul>
<li class="">Generates structured data using an LLM</li>
<li class="">Validates the result against custom rules</li>
<li class="">Retries automatically until the result passes</li>
</ul>
<p>You can try this exact workflow right now in your browser with no setup or coding required. Just click the button in the Workflow <a href="https://github.com/durable-workflow/sample-app/tree/Laravel-12" target="_blank" rel="noopener noreferrer" class="">Sample App</a> and launch a GitHub Codespace to run it.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-were-building">What We’re Building<a href="https://durable-workflow.com/uk/blog/building-reliable-agentic-loops-with-laravel-workflow-and-prismphp/#what-were-building" class="hash-link" aria-label="Пряме посилання на What We’re Building" title="Пряме посилання на What We’re Building" translate="no">​</a></h3>
<p>We’ll create a workflow that asks an LLM to generate a user profile with hobbies. Then we’ll validate that:</p>
<ul>
<li class="">The name is present</li>
<li class="">At least one hobby is defined</li>
<li class="">The name starts with a vowel</li>
</ul>
<p>If the result fails validation, we loop back to the LLM and regenerate. All of this is durable, asynchronous, and tracked through stateful events.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-by-step-example">Step-by-Step Example<a href="https://durable-workflow.com/uk/blog/building-reliable-agentic-loops-with-laravel-workflow-and-prismphp/#step-by-step-example" class="hash-link" aria-label="Пряме посилання на Step-by-Step Example" title="Пряме посилання на Step-by-Step Example" translate="no">​</a></h3>
<ol>
<li class="">Console Command to Trigger the Workflow</li>
</ol>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">use App\Workflows\Prism\PrismWorkflow;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Illuminate\Console\Command;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Workflow\WorkflowStub;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">class Prism extends Command</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    protected $signature = 'app:prism';</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    protected $description = 'Runs a Prism AI workflow';</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function handle()</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $workflow = WorkflowStub::make(PrismWorkflow::class);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $workflow-&gt;start();</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        while ($workflow-&gt;running());</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $user = $workflow-&gt;output();</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $this-&gt;info('Generated User:');</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $this-&gt;info(json_encode($user, JSON_PRETTY_PRINT));</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<ol start="2">
<li class="">Define the Workflow Logic</li>
</ol>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">use function Workflow\activity;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Workflow\Workflow;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">class PrismWorkflow extends Workflow</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function execute()</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        do {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            $user = yield activity(GenerateUserActivity::class);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            $valid = yield activity(ValidateUserActivity::class, $user);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        } while (!$valid);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        return $user;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<p>This is a classic agent loop. If validation fails, we prompt again automatically.</p>
<ol start="3">
<li class="">Generate Structured User Data with PrismPHP</li>
</ol>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Prism\Prism\Prism;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Prism\Prism\Enums\Provider;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Prism\Prism\Schema\ArraySchema;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Prism\Prism\Schema\ObjectSchema;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Prism\Prism\Schema\StringSchema;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Workflow\Activity;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">class GenerateUserActivity extends Activity</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function execute()</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $schema = new ObjectSchema(</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            name: 'user',</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            description: 'A user profile with their hobbies',</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            properties: [</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                new StringSchema('name', 'The user\'s full name'),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                new ArraySchema(</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                    name: 'hobbies',</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                    description: 'The user\'s list of hobbies',</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                    items: new ObjectSchema(</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                        name: 'hobby',</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                        description: 'A detailed hobby entry',</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                        properties: [</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                            new StringSchema('name', 'The name of the hobby'),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                            new StringSchema('description', 'A brief description of the hobby'),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                        ],</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                        requiredFields: ['name', 'description']</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                    )</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                ),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            ],</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            requiredFields: ['name', 'hobbies']</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        );</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $response = Prism::structured()</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            -&gt;using(Provider::OpenAI, 'gpt-4o')</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            -&gt;withSchema($schema)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            -&gt;withPrompt('Use names from many languages and vary first initials.')</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            -&gt;asStructured();</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        return $response-&gt;structured;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<ol start="4">
<li class="">Validate Business Logic</li>
</ol>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Workflow\Activity;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">class ValidateUserActivity extends Activity</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function execute($user)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        if (empty($user['name']) || !is_array($user['hobbies']) || count($user['hobbies']) === 0) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            return false;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        foreach ($user['hobbies'] as $hobby) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            if (empty($hobby['name']) || empty($hobby['description'])) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                return false;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        // Extra Validation: The user's name must start with a vowel.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        if (!in_array(strtoupper($user['name'][0]), ['A', 'E', 'I', 'O', 'U'])) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            return false;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        return true;  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-makes-this-pattern-powerful">What Makes This Pattern Powerful<a href="https://durable-workflow.com/uk/blog/building-reliable-agentic-loops-with-laravel-workflow-and-prismphp/#what-makes-this-pattern-powerful" class="hash-link" aria-label="Пряме посилання на What Makes This Pattern Powerful" title="Пряме посилання на What Makes This Pattern Powerful" translate="no">​</a></h3>
<p>This design pattern is what you’d call a reliable agentic loop:</p>
<ul>
<li class="">LLM generation via Prism</li>
<li class="">Validation &amp; retry via Workflow</li>
<li class="">State persistence for crash recovery or inspection</li>
<li class="">Observability via Waterline</li>
</ul>
<p>It’s perfect for AI applications where accuracy, safety, and traceability are required.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="try-it-now-in-your-browser">Try It Now in Your Browser<a href="https://durable-workflow.com/uk/blog/building-reliable-agentic-loops-with-laravel-workflow-and-prismphp/#try-it-now-in-your-browser" class="hash-link" aria-label="Пряме посилання на Try It Now in Your Browser" title="Пряме посилання на Try It Now in Your Browser" translate="no">​</a></h3>
<p>We’ve bundled this workflow into the official Workflow <a href="https://github.com/durable-workflow/sample-app/tree/Laravel-12" target="_blank" rel="noopener noreferrer" class="">Sample App</a>.</p>
<p>To try it:</p>
<ol>
<li class="">Open the sample-app repo's <code>Laravel-12</code> branch on GitHub</li>
<li class="">Click <strong>Code</strong> → <strong>Codespaces</strong> → <strong>Create codespace on Laravel-12</strong></li>
<li class="">Wait for the environment to build</li>
<li class="">Set your OPENAI_API_KEY in the .env</li>
<li class="">Setup the app and start the queue worker:<!-- -->
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">php artisan app:init</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">php artisan queue:work</span><br></div></code></pre></div></div>
</li>
<li class="">In a second terminal:</li>
</ol>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">php artisan app:prism</span><br></div></code></pre></div></div>
<p>You will see the queue working and eventually see the validated output.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="where-to-go-from-here">Where to Go From Here<a href="https://durable-workflow.com/uk/blog/building-reliable-agentic-loops-with-laravel-workflow-and-prismphp/#where-to-go-from-here" class="hash-link" aria-label="Пряме посилання на Where to Go From Here" title="Пряме посилання на Where to Go From Here" translate="no">​</a></h3>
<p>You can easily adapt this pattern to:</p>
<ul>
<li class="">AI agents for form filling</li>
<li class="">Data scraping and validation</li>
<li class="">Content generation with retry policies</li>
<li class="">Moderation and review queues</li>
</ul>
<p>Each step remains reliable and traceable thanks to Workflow’s durable execution model.</p>]]></content>
        <author>
            <name>Richard</name>
            <uri>https://github.com/rmcdaniel</uri>
        </author>
        <category label="ai" term="ai"/>
        <category label="workflow" term="workflow"/>
        <category label="agents" term="agents"/>
        <category label="agentic" term="agentic"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Automating QA with Playwright and Workflow]]></title>
        <id>https://durable-workflow.com/uk/blog/automating-qa-with-playwright-and-laravel-workflow/</id>
        <link href="https://durable-workflow.com/uk/blog/automating-qa-with-playwright-and-laravel-workflow/"/>
        <updated>2025-02-07T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[playwright]]></summary>
        <content type="html"><![CDATA[<p><img decoding="async" loading="lazy" src="https://miro.medium.com/v2/resize:fit:300/format:webp/1*b6eXVs5J3aRNzYAiqnS9Vw.png" alt="playwright" class="img_ev3q"></p>
<p>Have you ever spent hours tracking down a frontend bug that only happens in production? When working with web applications, debugging frontend issues can be challenging. Console errors and unexpected UI behaviors often require careful inspection and reproducible test cases. Wouldn’t it be great if you could automate this process, capture errors, and even record a video of the session for later analysis?</p>
<p>With <strong>Playwright</strong> and <strong>Workflow</strong>, you can achieve just that! In this post, I’ll walk you through an automated workflow that:</p>
<ul>
<li class="">Loads a webpage and captures console errors.</li>
<li class="">Records a video of the session.</li>
<li class="">Converts the video to an MP4 format for easy sharing.</li>
<li class="">Runs seamlessly in a <strong>GitHub Codespace</strong>.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-stack">The Stack<a href="https://durable-workflow.com/uk/blog/automating-qa-with-playwright-and-laravel-workflow/#the-stack" class="hash-link" aria-label="Пряме посилання на The Stack" title="Пряме посилання на The Stack" translate="no">​</a></h2>
<ul>
<li class=""><strong>Playwright</strong>: A powerful browser automation tool for testing web applications.</li>
<li class=""><strong>Workflow</strong>: A durable workflow engine for handling long-running, distributed processes.</li>
<li class=""><strong>FFmpeg</strong>: Used to convert Playwright’s WebM recordings to MP4 format.</li>
</ul>
<div class="themedImageWrapper_nM_G"><div class="lightImage_srvP"><a href="https://mermaid.live/edit#pako:eNpNkl1v0zAUhv_K0bngKhtNSdYmQkMsbbZJFKYVMUHSC68-TS0SO3LtbqPtf8d2mMadj5_3PV86B1wrTpjjplVP6y3TBr7PagnwuVoaHz0o_duzFZydXcJVdW8l3LXs5UmLZmtgudaiN6taes-V1xwL1hurCQold6olmGut9O4IxWF4Qams5J9O3lEEx09yeFZ9Uc0_8eqNfVVHmFculxHSUgCz0EpZvbbmKnV9S2ag84H-39E9rZXm8ENwctmuqyXbEzzQ4wJK0Q6u6-C68YX25Mf21ChY3CWwF-zjo35_WZZdT02Q3wT5rduRcpO-g2_W9NZ4dcC3Qw8YYaMFx9xoSxF2pDvmQzx4UY1mSx3VmLsnpw2zramxlidn65n8pVT36tTKNlvMN6zducj2nBmaCdZo9iYhyUkXbrUG8yxkwPyAz5gno-w8i9M4HiXpeJpNxkmEL-774nw6SbJJOk3H8YeLSZycIvwTio4cSSMkLtx4i-E-wpmc_gKg97DW" target="_blank" rel="noopener noreferrer"><img src="https://mermaid.ink/img/pako:eNpNkl1v0zAUhv_K0bngKhtNSdYmQkMsbbZJFKYVMUHSC68-TS0SO3LtbqPtf8d2mMadj5_3PV86B1wrTpjjplVP6y3TBr7PagnwuVoaHz0o_duzFZydXcJVdW8l3LXs5UmLZmtgudaiN6taes-V1xwL1hurCQold6olmGut9O4IxWF4Qams5J9O3lEEx09yeFZ9Uc0_8eqNfVVHmFculxHSUgCz0EpZvbbmKnV9S2ag84H-39E9rZXm8ENwctmuqyXbEzzQ4wJK0Q6u6-C68YX25Mf21ChY3CWwF-zjo35_WZZdT02Q3wT5rduRcpO-g2_W9NZ4dcC3Qw8YYaMFx9xoSxF2pDvmQzx4UY1mSx3VmLsnpw2zramxlidn65n8pVT36tTKNlvMN6zducj2nBmaCdZo9iYhyUkXbrUG8yxkwPyAz5gno-w8i9M4HiXpeJpNxkmEL-774nw6SbJJOk3H8YeLSZycIvwTio4cSSMkLtx4i-E-wpmc_gKg97DW?type=png" alt="Playwright QA Workflow Diagram"></a></div><div class="darkImage_qlOL"><a href="https://mermaid.live/edit#pako:eNpNkl1P2zAUhv-KdS52FbrWNMGNEBOkDSDRDVEE2pJemOaQWiR25NoF1va_z3Zg252Pn_c9Xzo7WKkKIYXnRr2u1lwbcj8tJSHnxcL46FHpF8-W5OjojFwUd1aS24a_v2pRrw1ZrLTozLKU3nPhNfuMd8ZqJJmSG9UgmWmt9GZPsl3_Irmysvp28I4sOH6iw9PiRtUf4uU_9l3tyaxwuYyQFgOYhlby4rM1V6ntGjQ9nfX0_47ucKV0RR5EhS7bZbHgWySP-DQnuWh612VwXflCW_Rje2oUmd-OyVbw0yf99SzP2w7rIL8K8mu3I-Um_UJ-WNNZ49UBX_c9QAS1FhWkRluMoEXdch_CzotKMGtssYTUPSuuX0oo5cF5Oi5_KdV-2rSy9RrSZ95sXGS7ihucCl5r3v791Sgr1JlbrIGUxiEHpDt4g3SUxIPkhMUnI0bj4YQNkwjenYglg2QcJ2wypkMW0_j4EMHvUHY4iMfHE8omdEQpTVjCIsBKuFHn_a2Ekzn8AZMbsr4" target="_blank" rel="noopener noreferrer"><img src="https://mermaid.ink/img/pako:eNpNkl1P2zAUhv-KdS52FbrWNMGNEBOkDSDRDVEE2pJemOaQWiR25NoF1va_z3Zg252Pn_c9Xzo7WKkKIYXnRr2u1lwbcj8tJSHnxcL46FHpF8-W5OjojFwUd1aS24a_v2pRrw1ZrLTozLKU3nPhNfuMd8ZqJJmSG9UgmWmt9GZPsl3_Irmysvp28I4sOH6iw9PiRtUf4uU_9l3tyaxwuYyQFgOYhlby4rM1V6ntGjQ9nfX0_47ucKV0RR5EhS7bZbHgWySP-DQnuWh612VwXflCW_Rje2oUmd-OyVbw0yf99SzP2w7rIL8K8mu3I-Um_UJ-WNNZ49UBX_c9QAS1FhWkRluMoEXdch_CzotKMGtssYTUPSuuX0oo5cF5Oi5_KdV-2rSy9RrSZ95sXGS7ihucCl5r3v791Sgr1JlbrIGUxiEHpDt4g3SUxIPkhMUnI0bj4YQNkwjenYglg2QcJ2wypkMW0_j4EMHvUHY4iMfHE8omdEQpTVjCIsBKuFHn_a2Ekzn8AZMbsr4?type=png" alt="Playwright QA Workflow Diagram"></a></div></div>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-capturing-errors-and-video-with-playwright">1. Capturing Errors and Video with Playwright<a href="https://durable-workflow.com/uk/blog/automating-qa-with-playwright-and-laravel-workflow/#1-capturing-errors-and-video-with-playwright" class="hash-link" aria-label="Пряме посилання на 1. Capturing Errors and Video with Playwright" title="Пряме посилання на 1. Capturing Errors and Video with Playwright" translate="no">​</a></h4>
<p>The Playwright script automates a browser session, navigates to a given URL, and logs any console errors. It also records a video of the entire session.</p>
<div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token keyword module" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token imports"> chromium </span><span class="token imports punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"> </span><span class="token keyword module" style="color:rgb(189, 147, 249);font-style:italic">from</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">'playwright'</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword module" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> </span><span class="token imports">path</span><span class="token plain"> </span><span class="token keyword module" style="color:rgb(189, 147, 249);font-style:italic">from</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">'path'</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword module" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> </span><span class="token imports">fs</span><span class="token plain"> </span><span class="token keyword module" style="color:rgb(189, 147, 249);font-style:italic">from</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">'fs'</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">async</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token arrow operator">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">const</span><span class="token plain"> url </span><span class="token operator">=</span><span class="token plain"> process</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token property-access">argv</span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token number">2</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">const</span><span class="token plain"> videoDir </span><span class="token operator">=</span><span class="token plain"> path</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token method function property-access" style="color:rgb(80, 250, 123)">resolve</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">'./videos'</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword control-flow" style="color:rgb(189, 147, 249);font-style:italic">if</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token operator">!</span><span class="token plain">fs</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token method function property-access" style="color:rgb(80, 250, 123)">existsSync</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">videoDir</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        fs</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token method function property-access" style="color:rgb(80, 250, 123)">mkdirSync</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">videoDir</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> </span><span class="token literal-property property">recursive</span><span class="token operator">:</span><span class="token plain"> </span><span class="token boolean">true</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">const</span><span class="token plain"> browser </span><span class="token operator">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:rgb(189, 147, 249);font-style:italic">await</span><span class="token plain"> chromium</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token method function property-access" style="color:rgb(80, 250, 123)">launch</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> </span><span class="token literal-property property">args</span><span class="token operator">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token string" style="color:rgb(255, 121, 198)">'--no-sandbox'</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">const</span><span class="token plain"> context </span><span class="token operator">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:rgb(189, 147, 249);font-style:italic">await</span><span class="token plain"> browser</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token method function property-access" style="color:rgb(80, 250, 123)">newContext</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token literal-property property">recordVideo</span><span class="token operator">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> </span><span class="token literal-property property">dir</span><span class="token operator">:</span><span class="token plain"> videoDir </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">const</span><span class="token plain"> page </span><span class="token operator">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:rgb(189, 147, 249);font-style:italic">await</span><span class="token plain"> context</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token method function property-access" style="color:rgb(80, 250, 123)">newPage</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">let</span><span class="token plain"> errors </span><span class="token operator">=</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    page</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token method function property-access" style="color:rgb(80, 250, 123)">on</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">'console'</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token parameter">msg</span><span class="token plain"> </span><span class="token arrow operator">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token keyword control-flow" style="color:rgb(189, 147, 249);font-style:italic">if</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">msg</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token method function property-access" style="color:rgb(80, 250, 123)">type</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token operator">===</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">'error'</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            errors</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token method function property-access" style="color:rgb(80, 250, 123)">push</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">msg</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token method function property-access" style="color:rgb(80, 250, 123)">text</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword control-flow" style="color:rgb(189, 147, 249);font-style:italic">try</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token keyword control-flow" style="color:rgb(189, 147, 249);font-style:italic">await</span><span class="token plain"> page</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token method function property-access" style="color:rgb(80, 250, 123)">goto</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">url</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> </span><span class="token literal-property property">waitUntil</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">'networkidle'</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token literal-property property">timeout</span><span class="token operator">:</span><span class="token plain"> </span><span class="token number">10000</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:rgb(189, 147, 249);font-style:italic">catch</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">error</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        errors</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token method function property-access" style="color:rgb(80, 250, 123)">push</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token template-string template-punctuation string" style="color:rgb(255, 121, 198)">`</span><span class="token template-string string" style="color:rgb(255, 121, 198)">Page load error: </span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:rgb(248, 248, 242)">${</span><span class="token template-string interpolation">error</span><span class="token template-string interpolation punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token template-string interpolation property-access">message</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token template-string template-punctuation string" style="color:rgb(255, 121, 198)">`</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">const</span><span class="token plain"> video </span><span class="token operator">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:rgb(189, 147, 249);font-style:italic">await</span><span class="token plain"> page</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token method function property-access" style="color:rgb(80, 250, 123)">video</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token method function property-access" style="color:rgb(80, 250, 123)">path</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword control-flow" style="color:rgb(189, 147, 249);font-style:italic">await</span><span class="token plain"> browser</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token method function property-access" style="color:rgb(80, 250, 123)">close</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token console class-name">console</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token method function property-access" style="color:rgb(80, 250, 123)">log</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token known-class-name class-name">JSON</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token method function property-access" style="color:rgb(80, 250, 123)">stringify</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> errors</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> video </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><br></div></code></pre></div></div>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-running-the-workflow">2. Running the Workflow<a href="https://durable-workflow.com/uk/blog/automating-qa-with-playwright-and-laravel-workflow/#2-running-the-workflow" class="hash-link" aria-label="Пряме посилання на 2. Running the Workflow" title="Пряме посилання на 2. Running the Workflow" translate="no">​</a></h4>
<p>A Laravel console command (<code>php artisan app:playwright</code>) starts the workflow which:</p>
<ul>
<li class="">Runs the Playwright script and collects errors.</li>
<li class="">Converts the video from <code>.webm</code> to <code>.mp4</code> using FFmpeg.</li>
<li class="">Returns the errors and the final video file path.</li>
</ul>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">namespace App\Console\Commands;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use App\Workflows\Playwright\CheckConsoleErrorsWorkflow;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Illuminate\Console\Command;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Workflow\WorkflowStub;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">class Playwright extends Command</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    protected $signature = 'app:playwright';</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    protected $description = 'Runs a playwright workflow';</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function handle()</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $workflow = WorkflowStub::make(CheckConsoleErrorsWorkflow::class);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $workflow-&gt;start('https://example.com');</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        while ($workflow-&gt;running());</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $this-&gt;info($workflow-&gt;output()['mp4']);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="3-the-workflow">3. The Workflow<a href="https://durable-workflow.com/uk/blog/automating-qa-with-playwright-and-laravel-workflow/#3-the-workflow" class="hash-link" aria-label="Пряме посилання на 3. The Workflow" title="Пряме посилання на 3. The Workflow" translate="no">​</a></h4>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">namespace App\Workflows\Playwright;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use function Workflow\activity;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Workflow\Workflow;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">class CheckConsoleErrorsWorkflow extends Workflow</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function execute(string $url)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $result = yield activity(CheckConsoleErrorsActivity::class, $url);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $mp4 = yield activity(ConvertVideoActivity::class, $result['video']);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        return [</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'errors' =&gt; $result['errors'],</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'mp4' =&gt; $mp4,</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        ];</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="4-running-playwright">4. Running Playwright<a href="https://durable-workflow.com/uk/blog/automating-qa-with-playwright-and-laravel-workflow/#4-running-playwright" class="hash-link" aria-label="Пряме посилання на 4. Running Playwright" title="Пряме посилання на 4. Running Playwright" translate="no">​</a></h4>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">namespace App\Workflows\Playwright;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Illuminate\Support\Facades\Process;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Workflow\Activity;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">class CheckConsoleErrorsActivity extends Activity</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function execute(string $url)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $result = Process::run([</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'node', base_path('playwright-script.js'), $url</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        ])-&gt;throw();</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        return json_decode($result-&gt;output(), true);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="5-video-conversion-with-ffmpeg">5. Video Conversion with FFmpeg<a href="https://durable-workflow.com/uk/blog/automating-qa-with-playwright-and-laravel-workflow/#5-video-conversion-with-ffmpeg" class="hash-link" aria-label="Пряме посилання на 5. Video Conversion with FFmpeg" title="Пряме посилання на 5. Video Conversion with FFmpeg" translate="no">​</a></h4>
<p>The Playwright recording is stored in WebM format, but we need an MP4 for wider compatibility. Workflow runs this process asynchronously.</p>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">namespace App\Workflows\Playwright;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Illuminate\Support\Facades\Process;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Workflow\Activity;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">class ConvertVideoActivity extends Activity</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function execute(string $webm)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $mp4 = str_replace('.webm', '.mp4', $webm);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        Process::run([</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'ffmpeg', '-i', $webm, '-c:v', 'libx264', '-preset', 'fast', '-crf', '23', '-c:a', 'aac', '-b:a', '128k', $mp4</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        ])-&gt;throw();</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        unlink($webm);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        return $mp4;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="try-it-now-in-your-browser">Try It Now in Your Browser<a href="https://durable-workflow.com/uk/blog/automating-qa-with-playwright-and-laravel-workflow/#try-it-now-in-your-browser" class="hash-link" aria-label="Пряме посилання на Try It Now in Your Browser" title="Пряме посилання на Try It Now in Your Browser" translate="no">​</a></h2>
<p>You don’t need to set up anything on your local machine. Everything is already configured in the Workflow <a href="https://github.com/durable-workflow/sample-app/tree/Laravel-12" target="_blank" rel="noopener noreferrer" class="">Sample App</a>.</p>
<p>To try it:</p>
<ol>
<li class="">Open the sample-app repo's <code>Laravel-12</code> branch on GitHub</li>
<li class="">Click <strong>Code</strong> → <strong>Codespaces</strong> → <strong>Create codespace on Laravel-12</strong></li>
<li class="">Wait for the environment to build</li>
<li class="">Setup the app and start the queue worker:<!-- -->
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">php artisan app:init</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">php artisan queue:work</span><br></div></code></pre></div></div>
</li>
<li class="">In a second terminal:</li>
</ol>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">php artisan app:playwright</span><br></div></code></pre></div></div>
<p>That’s it! The workflow will execute, capture console errors, record a video, and convert it to MP4. You can find the video in the videos folder. Take a look at the sample app’s README.md for more information on other workflows and how to view the Waterline UI.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="conclusion">Conclusion<a href="https://durable-workflow.com/uk/blog/automating-qa-with-playwright-and-laravel-workflow/#conclusion" class="hash-link" aria-label="Пряме посилання на Conclusion" title="Пряме посилання на Conclusion" translate="no">​</a></h2>
<p>By integrating Playwright with Workflow, we’ve automated frontend error detection and debugging. This setup allows teams to quickly identify and resolve issues, all while leveraging Laravel’s queue system to run tasks asynchronously.</p>]]></content>
        <author>
            <name>Richard</name>
            <uri>https://github.com/rmcdaniel</uri>
        </author>
        <category label="playwright" term="playwright"/>
        <category label="workflow" term="workflow"/>
        <category label="automation" term="automation"/>
        <category label="qa" term="qa"/>
        <category label="testing" term="testing"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Extending Workflow to Support Spatie Laravel Tags]]></title>
        <id>https://durable-workflow.com/uk/blog/extending-laravel-workflow-to-support-spatie-laravel-tags/</id>
        <link href="https://durable-workflow.com/uk/blog/extending-laravel-workflow-to-support-spatie-laravel-tags/"/>
        <updated>2023-08-28T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[One of the strengths of the Laravel ecosystem is its flexibility, thanks to a myriad of community-driven packages that enhance the framework’s capabilities. The laravel-workflow and spatie/laravel-tags packages are two such examples, and in this post, we'll integrate them together to make workflows taggable.]]></summary>
        <content type="html"><![CDATA[<p>One of the strengths of the Laravel ecosystem is its flexibility, thanks to a myriad of community-driven packages that enhance the framework’s capabilities. The <code>laravel-workflow</code> and <code>spatie/laravel-tags</code> packages are two such examples, and in this post, we'll integrate them together to make workflows taggable.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="installation-instructions">Installation Instructions<a href="https://durable-workflow.com/uk/blog/extending-laravel-workflow-to-support-spatie-laravel-tags/#installation-instructions" class="hash-link" aria-label="Пряме посилання на Installation Instructions" title="Пряме посилання на Installation Instructions" translate="no">​</a></h2>
<p>Before diving into the code, let’s ensure both libraries are properly installed:</p>
<ol>
<li class="">Install <a href="https://github.com/durable-workflow/workflow" target="_blank" rel="noopener noreferrer" class="">Workflow</a> and <a href="https://github.com/spatie/laravel-tags" target="_blank" rel="noopener noreferrer" class="">Spatie Laravel Tags</a>.</li>
</ol>
<div class="language-sh codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sh codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">composer require laravel-workflow/laravel-workflow spatie/laravel-tags</span><br></div></code></pre></div></div>
<ol start="2">
<li class="">Both packages include migrations that must be published.</li>
</ol>
<div class="language-sh codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sh codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">php artisan vendor:publish --provider="Workflow\Providers\WorkflowServiceProvider" --tag="migrations"</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">php artisan vendor:publish --provider="Spatie\Tags\TagsServiceProvider" --tag="tags-migrations"</span><br></div></code></pre></div></div>
<ol start="3">
<li class="">Run the migrations.</li>
</ol>
<div class="language-sh codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sh codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">php artisan migrate</span><br></div></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="publishing-configuration">Publishing Configuration<a href="https://durable-workflow.com/uk/blog/extending-laravel-workflow-to-support-spatie-laravel-tags/#publishing-configuration" class="hash-link" aria-label="Пряме посилання на Publishing Configuration" title="Пряме посилання на Publishing Configuration" translate="no">​</a></h2>
<p>To extend Workflow, publish its configuration file:</p>
<div class="language-sh codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sh codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">php artisan vendor:publish --provider="Workflow\Providers\WorkflowServiceProvider" --tag="config"</span><br></div></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="extending-workflows-to-support-tags">Extending Workflows to Support Tags<a href="https://durable-workflow.com/uk/blog/extending-laravel-workflow-to-support-spatie-laravel-tags/#extending-workflows-to-support-tags" class="hash-link" aria-label="Пряме посилання на Extending Workflows to Support Tags" title="Пряме посилання на Extending Workflows to Support Tags" translate="no">​</a></h2>
<p>We need to extend the <code>StoredWorkflow</code> model of <code>laravel-workflow</code> to support tagging.</p>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">namespace App\Models;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Spatie\Tags\HasTags;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Workflow\Models\StoredWorkflow as BaseStoredWorkflow;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Workflow\WorkflowStub;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">class StoredWorkflow extends BaseStoredWorkflow</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    use HasTags;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public static function tag(WorkflowStub $workflow, $tag): void</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $storedWorkflow = static::find($workflow-&gt;id());</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        if ($storedWorkflow) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            $storedWorkflow-&gt;attachTag($tag);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public static function findByTag($tag): ?WorkflowStub</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $storedWorkflow = static::withAnyTags([$tag])-&gt;first();</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        if ($storedWorkflow) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            return WorkflowStub::fromStoredWorkflow($storedWorkflow);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="modify-the-configuration">Modify the Configuration<a href="https://durable-workflow.com/uk/blog/extending-laravel-workflow-to-support-spatie-laravel-tags/#modify-the-configuration" class="hash-link" aria-label="Пряме посилання на Modify the Configuration" title="Пряме посилання на Modify the Configuration" translate="no">​</a></h2>
<p>In <code>config/workflow.php</code>, update this line:</p>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">'stored_workflow_model' =&gt; Workflow\Models\StoredWorkflow::class,</span><br></div></code></pre></div></div>
<p>To:</p>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">'stored_workflow_model' =&gt; App\Models\StoredWorkflow::class,</span><br></div></code></pre></div></div>
<p>This ensures Workflow uses the extended model.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="running-tagged-workflows">Running Tagged Workflows<a href="https://durable-workflow.com/uk/blog/extending-laravel-workflow-to-support-spatie-laravel-tags/#running-tagged-workflows" class="hash-link" aria-label="Пряме посилання на Running Tagged Workflows" title="Пряме посилання на Running Tagged Workflows" translate="no">​</a></h2>
<p>With the taggable <code>StoredWorkflow</code> ready, create a console command to create, tag, retrieve, and run a workflow.</p>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">namespace App\Console\Commands;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use App\Models\StoredWorkflow;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use App\Workflows\Simple\SimpleWorkflow;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Illuminate\Console\Command;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Workflow\WorkflowStub;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">class Workflow extends Command</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    protected $signature = 'workflow';</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    protected $description = 'Runs a workflow';</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function handle()</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        // Create a workflow and tag it</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $workflow = WorkflowStub::make(SimpleWorkflow::class);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        StoredWorkflow::tag($workflow, 'tag1');</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        // Find the workflow by tag and start it</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $workflow = StoredWorkflow::findByTag('tag1');</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $workflow-&gt;start();</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        while ($workflow-&gt;running());</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $this-&gt;info($workflow-&gt;output());</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="conclusion">Conclusion<a href="https://durable-workflow.com/uk/blog/extending-laravel-workflow-to-support-spatie-laravel-tags/#conclusion" class="hash-link" aria-label="Пряме посилання на Conclusion" title="Пряме посилання на Conclusion" translate="no">​</a></h2>
<p>By integrating <code>laravel-workflow</code> with <code>spatie/laravel-tags</code>, we've enabled tagging for workflows, making management more intuitive in larger applications. Thanks to Laravel’s extensible nature, endless possibilities await developers leveraging these powerful packages.</p>]]></content>
        <author>
            <name>Richard</name>
            <uri>https://github.com/rmcdaniel</uri>
        </author>
        <category label="laravel" term="laravel"/>
        <category label="workflow" term="workflow"/>
        <category label="spatie" term="spatie"/>
        <category label="tags" term="tags"/>
        <category label="automation" term="automation"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[AI Image Moderation with Workflow]]></title>
        <id>https://durable-workflow.com/uk/blog/ai-image-moderation-with-laravel-workflow/</id>
        <link href="https://durable-workflow.com/uk/blog/ai-image-moderation-with-laravel-workflow/"/>
        <updated>2023-08-20T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Before we begin, let’s understand the scenario. We are building an image moderation system where:]]></summary>
        <content type="html"><![CDATA[<p>Before we begin, let’s understand the scenario. We are building an image moderation system where:</p>
<ol>
<li class="">Every image undergoes an initial AI check to determine if it’s safe.</li>
<li class="">If the AI deems the image unsafe, it’s automatically logged and deleted.</li>
<li class="">If it’s potentially safe, a human moderator is alerted to further review the image. They have the option to approve or reject the image.</li>
<li class="">Approved images are moved to a public location, whereas rejected images are deleted.</li>
</ol>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="workflow">Workflow<a href="https://durable-workflow.com/uk/blog/ai-image-moderation-with-laravel-workflow/#workflow" class="hash-link" aria-label="Пряме посилання на Workflow" title="Пряме посилання на Workflow" translate="no">​</a></h2>
<p>Workflow is designed to streamline and organize complex processes in applications. It allows developers to define, manage, and execute workflows seamlessly. You can find installation instructions <a href="https://github.com/durable-workflow/workflow" target="_blank" rel="noopener noreferrer" class="">here</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="clarifai-api">ClarifAI API<a href="https://durable-workflow.com/uk/blog/ai-image-moderation-with-laravel-workflow/#clarifai-api" class="hash-link" aria-label="Пряме посилання на ClarifAI API" title="Пряме посилання на ClarifAI API" translate="no">​</a></h2>
<p>ClarifAI provides AI-powered moderation tools for analyzing visual content. They offer a <a href="https://www.clarifai.com/pricing" target="_blank" rel="noopener noreferrer" class="">free plan</a> with up to 1,000 actions per month.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-store-your-credentials-in-env">1. Store your credentials in <code>.env</code>.<a href="https://durable-workflow.com/uk/blog/ai-image-moderation-with-laravel-workflow/#1-store-your-credentials-in-env" class="hash-link" aria-label="Пряме посилання на 1-store-your-credentials-in-env" title="Пряме посилання на 1-store-your-credentials-in-env" translate="no">​</a></h4>
<div class="language-ini codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-ini codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">CLARIFAI_API_KEY=key</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">CLARIFAI_APP=my-application</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">CLARIFAI_WORKFLOW=my-workflow</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">CLARIFAI_USER=username</span><br></div></code></pre></div></div>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-add-the-service-to-configservicesphp">2. Add the service to <code>config/services.php</code>.<a href="https://durable-workflow.com/uk/blog/ai-image-moderation-with-laravel-workflow/#2-add-the-service-to-configservicesphp" class="hash-link" aria-label="Пряме посилання на 2-add-the-service-to-configservicesphp" title="Пряме посилання на 2-add-the-service-to-configservicesphp" translate="no">​</a></h4>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">'clarifai' =&gt; [</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    'api_key' =&gt; env('CLARIFAI_API_KEY'),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    'app' =&gt; env('CLARIFAI_APP'),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    'workflow' =&gt; env('CLARIFAI_WORKFLOW'),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    'user' =&gt; env('CLARIFAI_USER'),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">],</span><br></div></code></pre></div></div>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="3-create-a-service-at-appservicesclarifaiphp">3. Create a service at <code>app/Services/ClarifAI.php</code>.<a href="https://durable-workflow.com/uk/blog/ai-image-moderation-with-laravel-workflow/#3-create-a-service-at-appservicesclarifaiphp" class="hash-link" aria-label="Пряме посилання на 3-create-a-service-at-appservicesclarifaiphp" title="Пряме посилання на 3-create-a-service-at-appservicesclarifaiphp" translate="no">​</a></h4>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">namespace App\Services;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Illuminate\Support\Facades\Http;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">class ClarifAI</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    private $apiKey;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    private $apiUrl;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function __construct()</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $app = config('services.clarifai.app');</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $workflow = config('services.clarifai.workflow');</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $user = config('services.clarifai.user');</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $this-&gt;apiKey = config('services.clarifai.api_key');</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $this-&gt;apiUrl = "https://api.clarifai.com/v2/users/{$user}/apps/{$app}/workflows/{$workflow}/results/";</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function checkImage(string $image): bool</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $response = Http::withToken($this-&gt;apiKey, 'Key')</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            -&gt;post($this-&gt;apiUrl, ['inputs' =&gt; [</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                ['data' =&gt; ['image' =&gt; ['base64' =&gt; base64_encode($image)]]],</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            ]]);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        return collect($response-&gt;json('results.0.outputs.0.data.concepts', []))</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            -&gt;filter(fn ($value) =&gt; $value['name'] === 'safe')</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            -&gt;map(fn ($value) =&gt; round((float) $value['value']) &gt; 0)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            -&gt;first() ?? false;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="creating-the-workflow">Creating the Workflow<a href="https://durable-workflow.com/uk/blog/ai-image-moderation-with-laravel-workflow/#creating-the-workflow" class="hash-link" aria-label="Пряме посилання на Creating the Workflow" title="Пряме посилання на Creating the Workflow" translate="no">​</a></h2>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">namespace App\Workflows;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Workflow\SignalMethod;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Workflow\Workflow;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use function Workflow\{activity, all, awaitWithTimeout};</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">class ImageModerationWorkflow extends Workflow</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    private bool $approved = false;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    private bool $rejected = false;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    #[SignalMethod]</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function approve()</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $this-&gt;approved = true;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    #[SignalMethod]</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function reject()</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $this-&gt;rejected = true;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function execute($imagePath)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $safe = yield from $this-&gt;check($imagePath);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        if (! $safe) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            yield from $this-&gt;unsafe($imagePath);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            return 'unsafe';</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        yield from $this-&gt;moderate($imagePath);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        return $this-&gt;approved ? 'approved' : 'rejected';</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    private function check($imagePath)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        return yield activity(AutomatedImageCheckActivity::class, $imagePath);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    private function unsafe($imagePath)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        yield all([</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            activity(LogUnsafeImageActivity::class, $imagePath),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            activity(DeleteImageActivity::class, $imagePath),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        ]);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    private function moderate($imagePath)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        while (true) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            yield activity(NotifyImageModeratorActivity::class, $imagePath);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            $signaled = yield awaitWithTimeout('24 hours', fn () =&gt; $this-&gt;approved || $this-&gt;rejected);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            if ($signaled) break;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="activities">Activities<a href="https://durable-workflow.com/uk/blog/ai-image-moderation-with-laravel-workflow/#activities" class="hash-link" aria-label="Пряме посилання на Activities" title="Пряме посилання на Activities" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="automated-image-check">Automated Image Check<a href="https://durable-workflow.com/uk/blog/ai-image-moderation-with-laravel-workflow/#automated-image-check" class="hash-link" aria-label="Пряме посилання на Automated Image Check" title="Пряме посилання на Automated Image Check" translate="no">​</a></h3>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">namespace App\Workflows;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use App\Services\ClarifAI;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Illuminate\Support\Facades\Storage;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Workflow\Activity;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">class AutomatedImageCheckActivity extends Activity</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function execute($imagePath)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        return app(ClarifAI::class)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            -&gt;checkImage(Storage::get($imagePath));</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="logging-unsafe-images">Logging Unsafe Images<a href="https://durable-workflow.com/uk/blog/ai-image-moderation-with-laravel-workflow/#logging-unsafe-images" class="hash-link" aria-label="Пряме посилання на Logging Unsafe Images" title="Пряме посилання на Logging Unsafe Images" translate="no">​</a></h3>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">namespace App\Workflows;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Illuminate\Support\Facades\Log;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Workflow\Activity;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">class LogUnsafeImageActivity extends Activity</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function execute($imagePath)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        Log::info('Unsafe image detected at: ' . $imagePath);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="deleting-images">Deleting Images<a href="https://durable-workflow.com/uk/blog/ai-image-moderation-with-laravel-workflow/#deleting-images" class="hash-link" aria-label="Пряме посилання на Deleting Images" title="Пряме посилання на Deleting Images" translate="no">​</a></h3>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">namespace App\Workflows;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Illuminate\Support\Facades\Storage;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Workflow\Activity;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">class DeleteImageActivity extends Activity</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function execute($imagePath)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        Storage::delete($imagePath);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="starting-and-signaling-the-workflow">Starting and Signaling the Workflow<a href="https://durable-workflow.com/uk/blog/ai-image-moderation-with-laravel-workflow/#starting-and-signaling-the-workflow" class="hash-link" aria-label="Пряме посилання на Starting and Signaling the Workflow" title="Пряме посилання на Starting and Signaling the Workflow" translate="no">​</a></h2>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">$workflow = WorkflowStub::make(ImageModerationWorkflow::class);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">$workflow-&gt;start('tmp/good.jpg');</span><br></div></code></pre></div></div>
<p>For approvals or rejections:</p>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">$workflow = WorkflowStub::load($id);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">$workflow-&gt;approve();</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">// or</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">$workflow-&gt;reject();</span><br></div></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="conclusion">Conclusion<a href="https://durable-workflow.com/uk/blog/ai-image-moderation-with-laravel-workflow/#conclusion" class="hash-link" aria-label="Пряме посилання на Conclusion" title="Пряме посилання на Conclusion" translate="no">​</a></h2>
<p><a href="https://github.com/durable-workflow/workflow" target="_blank" rel="noopener noreferrer" class="">Workflow</a> provides a structured approach to handle complex processes like image moderation. It supports asynchronous processing, external API integrations, and modular design for scalability. Thanks for reading!</p>]]></content>
        <author>
            <name>Richard</name>
            <uri>https://github.com/rmcdaniel</uri>
        </author>
        <category label="ai" term="ai"/>
        <category label="image-moderation" term="image-moderation"/>
        <category label="workflow" term="workflow"/>
        <category label="automation" term="automation"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Microservice Communication with Workflow]]></title>
        <id>https://durable-workflow.com/uk/blog/microservice-communication-with-laravel-workflow/</id>
        <link href="https://durable-workflow.com/uk/blog/microservice-communication-with-laravel-workflow/"/>
        <updated>2023-08-18T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In the evolving landscape of microservices, communication has always been a focal point. Microservices can interact in various ways, be it through HTTP/REST calls, using messaging protocols like RabbitMQ or Kafka, or even employing more recent technologies like gRPC. Yet, regardless of the communication method, the goal remains the same: seamless, efficient, and robust interactions. Today, we’ll explore how Workflow can fit into this picture and optimize the communication between microservices in a unique way.]]></summary>
        <content type="html"><![CDATA[<p>In the evolving landscape of microservices, communication has always been a focal point. Microservices can interact in various ways, be it through HTTP/REST calls, using messaging protocols like RabbitMQ or Kafka, or even employing more recent technologies like gRPC. Yet, regardless of the communication method, the goal remains the same: seamless, efficient, and robust interactions. Today, we’ll explore how Workflow can fit into this picture and optimize the communication between microservices in a unique way.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-challenge">The Challenge<a href="https://durable-workflow.com/uk/blog/microservice-communication-with-laravel-workflow/#the-challenge" class="hash-link" aria-label="Пряме посилання на The Challenge" title="Пряме посилання на The Challenge" translate="no">​</a></h2>
<p>In a microservices architecture, decoupling is the name of the game. You want each service to have a single responsibility, to be maintainable, and to be independently deployable. Yet, in the world of workflows, this becomes challenging. How do you split a workflow from its activity and yet ensure they communicate seamlessly?</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="workflow-to-the-rescue">Workflow to the Rescue!<a href="https://durable-workflow.com/uk/blog/microservice-communication-with-laravel-workflow/#workflow-to-the-rescue" class="hash-link" aria-label="Пряме посилання на Workflow to the Rescue!" title="Пряме посилання на Workflow to the Rescue!" translate="no">​</a></h2>
<p><a href="https://github.com/durable-workflow/workflow" target="_blank" rel="noopener noreferrer" class="">Workflow</a> handles the discovery and orchestration for you! With a shared database and queue connection, you can have your workflow in one Laravel app and its activity logic in another.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="defining-workflows-and-activities">Defining Workflows and Activities<a href="https://durable-workflow.com/uk/blog/microservice-communication-with-laravel-workflow/#defining-workflows-and-activities" class="hash-link" aria-label="Пряме посилання на Defining Workflows and Activities" title="Пряме посилання на Defining Workflows and Activities" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-create-a-workflow">1. Create a workflow.<a href="https://durable-workflow.com/uk/blog/microservice-communication-with-laravel-workflow/#1-create-a-workflow" class="hash-link" aria-label="Пряме посилання на 1. Create a workflow." title="Пряме посилання на 1. Create a workflow." translate="no">​</a></h4>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">use function Workflow\activity;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Workflow\Workflow;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">class MyWorkflow extends Workflow</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function execute($name)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $result = yield activity(MyActivity::class, $name);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        return $result;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-create-an-activity">2. Create an activity.<a href="https://durable-workflow.com/uk/blog/microservice-communication-with-laravel-workflow/#2-create-an-activity" class="hash-link" aria-label="Пряме посилання на 2. Create an activity." title="Пряме посилання на 2. Create an activity." translate="no">​</a></h4>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Workflow\Activity;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">class MyActivity extends Activity</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function execute($name)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        return "Hello, {$name}!";</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="3-run-the-workflow">3. Run the workflow.<a href="https://durable-workflow.com/uk/blog/microservice-communication-with-laravel-workflow/#3-run-the-workflow" class="hash-link" aria-label="Пряме посилання на 3. Run the workflow." title="Пряме посилання на 3. Run the workflow." translate="no">​</a></h4>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Workflow\WorkflowStub;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">$workflow = WorkflowStub::make(MyWorkflow::class);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">$workflow-&gt;start('world');</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">while ($workflow-&gt;running());</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">$workflow-&gt;output();</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">// Output: 'Hello, world!'</span><br></div></code></pre></div></div>
<p>The workflow will manage the activity and handle any failures, retries, etc. Think of workflows like job chaining on steroids because you can have conditional logic, loops, return a result that can be used in the next activity, and write everything in typical PHP code that is failure tolerant.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="balancing-shared-and-dedicated-resources">Balancing Shared and Dedicated Resources<a href="https://durable-workflow.com/uk/blog/microservice-communication-with-laravel-workflow/#balancing-shared-and-dedicated-resources" class="hash-link" aria-label="Пряме посилання на Balancing Shared and Dedicated Resources" title="Пряме посилання на Balancing Shared and Dedicated Resources" translate="no">​</a></h2>
<p>When working with microservices, it’s common for each service to have its dedicated resources, such as databases, caches, and queues. However, to facilitate communication between workflows and activities across services, a shared connection (like a database or queue) becomes essential. This shared connection acts as a bridge for data and task exchanges while ensuring:</p>
<ol>
<li class=""><strong>Isolation</strong>: Dedicated resources prevent cascading failures.</li>
<li class=""><strong>Performance</strong>: Each service can be optimized independently.</li>
<li class=""><strong>Security</strong>: Isolation limits potential attack vectors.</li>
</ol>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-by-step-integration">Step-By-Step Integration<a href="https://durable-workflow.com/uk/blog/microservice-communication-with-laravel-workflow/#step-by-step-integration" class="hash-link" aria-label="Пряме посилання на Step-By-Step Integration" title="Пряме посилання на Step-By-Step Integration" translate="no">​</a></h2>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-install-laravel-workflow-in-all-microservices">1. Install <code>laravel-workflow</code> in all microservices.<a href="https://durable-workflow.com/uk/blog/microservice-communication-with-laravel-workflow/#1-install-laravel-workflow-in-all-microservices" class="hash-link" aria-label="Пряме посилання на 1-install-laravel-workflow-in-all-microservices" title="Пряме посилання на 1-install-laravel-workflow-in-all-microservices" translate="no">​</a></h4>
<p>Follow the <a href="https://durable-workflow.com/docs/installation/" target="_blank" rel="noopener noreferrer" class="">installation guide</a>.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-create-a-shared-databaseredis-connection-in-all-microservices">2. Create a shared database/redis connection in all microservices.<a href="https://durable-workflow.com/uk/blog/microservice-communication-with-laravel-workflow/#2-create-a-shared-databaseredis-connection-in-all-microservices" class="hash-link" aria-label="Пряме посилання на 2. Create a shared database/redis connection in all microservices." title="Пряме посилання на 2. Create a shared database/redis connection in all microservices." translate="no">​</a></h4>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">// config/database.php</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">'connections' =&gt; [</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    'shared' =&gt; [</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        'driver' =&gt; 'mysql',</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        'host' =&gt; env('SHARED_DB_HOST', '127.0.0.1'),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        'database' =&gt; env('SHARED_DB_DATABASE', 'forge'),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        'username' =&gt; env('SHARED_DB_USERNAME', 'forge'),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        'password' =&gt; env('SHARED_DB_PASSWORD', ''),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    ],</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">],</span><br></div></code></pre></div></div>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="3-configure-a-shared-queue-connection">3. Configure a shared queue connection.<a href="https://durable-workflow.com/uk/blog/microservice-communication-with-laravel-workflow/#3-configure-a-shared-queue-connection" class="hash-link" aria-label="Пряме посилання на 3. Configure a shared queue connection." title="Пряме посилання на 3. Configure a shared queue connection." translate="no">​</a></h4>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">// config/queue.php</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">'connections' =&gt; [</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    'shared' =&gt; [</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        'driver' =&gt; 'redis',</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        'connection' =&gt; 'shared',</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        'queue' =&gt; env('SHARED_REDIS_QUEUE', 'default'),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    ],</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">],</span><br></div></code></pre></div></div>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="4-ensure-only-one-microservice-publishes-workflow-migrations">4. Ensure only one microservice publishes Workflow migrations.<a href="https://durable-workflow.com/uk/blog/microservice-communication-with-laravel-workflow/#4-ensure-only-one-microservice-publishes-workflow-migrations" class="hash-link" aria-label="Пряме посилання на 4. Ensure only one microservice publishes Workflow migrations." title="Пряме посилання на 4. Ensure only one microservice publishes Workflow migrations." translate="no">​</a></h4>
<p>Update the migration to use the shared database connection.</p>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">// database/migrations/..._create_workflows_table.php</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">class CreateWorkflowsTable extends Migration</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    protected $connection = 'shared';</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="5-extend-workflow-models-in-each-microservice-to-use-the-shared-connection">5. Extend workflow models in each microservice to use the shared connection.<a href="https://durable-workflow.com/uk/blog/microservice-communication-with-laravel-workflow/#5-extend-workflow-models-in-each-microservice-to-use-the-shared-connection" class="hash-link" aria-label="Пряме посилання на 5. Extend workflow models in each microservice to use the shared connection." title="Пряме посилання на 5. Extend workflow models in each microservice to use the shared connection." translate="no">​</a></h4>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">// app/Models/StoredWorkflow.php</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">namespace App\Models;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Workflow\Models\StoredWorkflow as BaseStoredWorkflow;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">class StoredWorkflow extends BaseStoredWorkflow</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    protected $connection = 'shared';</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="6-publish-workflow-config-and-update-it-with-shared-models">6. Publish Workflow config and update it with shared models.<a href="https://durable-workflow.com/uk/blog/microservice-communication-with-laravel-workflow/#6-publish-workflow-config-and-update-it-with-shared-models" class="hash-link" aria-label="Пряме посилання на 6. Publish Workflow config and update it with shared models." title="Пряме посилання на 6. Publish Workflow config and update it with shared models." translate="no">​</a></h4>
<div class="language-sh codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sh codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">php artisan vendor:publish --provider="Workflow\Providers\WorkflowServiceProvider" --tag="config"</span><br></div></code></pre></div></div>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="7-set-workflows-and-activities-to-use-the-shared-queue">7. Set workflows and activities to use the shared queue.<a href="https://durable-workflow.com/uk/blog/microservice-communication-with-laravel-workflow/#7-set-workflows-and-activities-to-use-the-shared-queue" class="hash-link" aria-label="Пряме посилання на 7. Set workflows and activities to use the shared queue." title="Пряме посилання на 7. Set workflows and activities to use the shared queue." translate="no">​</a></h4>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">// app/Workflows/MyWorkflow.php</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">class MyWorkflow extends Workflow</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public $connection = 'shared';</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public $queue = 'workflow';</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">// app/Workflows/MyActivity.php</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">class MyActivity extends Activity</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public $connection = 'shared';</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public $queue = 'activity';</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="8-ensure-microservices-define-empty-counterparts-for-workflow-and-activity-classes">8. Ensure microservices define empty counterparts for workflow and activity classes.<a href="https://durable-workflow.com/uk/blog/microservice-communication-with-laravel-workflow/#8-ensure-microservices-define-empty-counterparts-for-workflow-and-activity-classes" class="hash-link" aria-label="Пряме посилання на 8. Ensure microservices define empty counterparts for workflow and activity classes." title="Пряме посилання на 8. Ensure microservices define empty counterparts for workflow and activity classes." translate="no">​</a></h4>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="in-the-workflow-microservice">In the workflow microservice:<a href="https://durable-workflow.com/uk/blog/microservice-communication-with-laravel-workflow/#in-the-workflow-microservice" class="hash-link" aria-label="Пряме посилання на In the workflow microservice:" title="Пряме посилання на In the workflow microservice:" translate="no">​</a></h4>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">class MyWorkflow extends Workflow</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public $connection = 'shared';</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public $queue = 'workflow';</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function execute($name)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        yield activity(MyActivity::class, $name);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">class MyActivity extends Activity</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public $connection = 'shared';</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public $queue = 'activity';</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="in-the-activity-microservice">In the activity microservice:<a href="https://durable-workflow.com/uk/blog/microservice-communication-with-laravel-workflow/#in-the-activity-microservice" class="hash-link" aria-label="Пряме посилання на In the activity microservice:" title="Пряме посилання на In the activity microservice:" translate="no">​</a></h4>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">class MyWorkflow extends Workflow</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public $connection = 'shared';</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public $queue = 'workflow';</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">class MyActivity extends Activity</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public $connection = 'shared';</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public $queue = 'activity';</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function execute($name)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        return "Hello, {$name}!";</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="9-ensure-all-microservices-have-the-same-app_key-in-their-env-file">9. Ensure all microservices have the same <code>APP_KEY</code> in their <code>.env</code> file.<a href="https://durable-workflow.com/uk/blog/microservice-communication-with-laravel-workflow/#9-ensure-all-microservices-have-the-same-app_key-in-their-env-file" class="hash-link" aria-label="Пряме посилання на 9-ensure-all-microservices-have-the-same-app_key-in-their-env-file" title="Пряме посилання на 9-ensure-all-microservices-have-the-same-app_key-in-their-env-file" translate="no">​</a></h4>
<p>This is crucial for proper job serialization across services.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="10-run-queue-workers-in-each-microservice">10. Run queue workers in each microservice.<a href="https://durable-workflow.com/uk/blog/microservice-communication-with-laravel-workflow/#10-run-queue-workers-in-each-microservice" class="hash-link" aria-label="Пряме посилання на 10. Run queue workers in each microservice." title="Пряме посилання на 10. Run queue workers in each microservice." translate="no">​</a></h4>
<div class="language-sh codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sh codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">php artisan queue:work shared --queue=workflow</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">php artisan queue:work shared --queue=activity</span><br></div></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="conclusion">Conclusion<a href="https://durable-workflow.com/uk/blog/microservice-communication-with-laravel-workflow/#conclusion" class="hash-link" aria-label="Пряме посилання на Conclusion" title="Пряме посилання на Conclusion" translate="no">​</a></h2>
<p>By following the steps above, you can ensure seamless interactions between microservices while maintaining modularity and scalability. Workflow takes care of the discovery and orchestration for you. 🚀</p>
<p>Thanks for reading!</p>]]></content>
        <author>
            <name>Richard</name>
            <uri>https://github.com/rmcdaniel</uri>
        </author>
        <category label="microservices" term="microservices"/>
        <category label="workflow" term="workflow"/>
        <category label="communication" term="communication"/>
        <category label="distributed-systems" term="distributed-systems"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Saga Pattern and Workflow]]></title>
        <id>https://durable-workflow.com/uk/blog/saga-pattern-and-laravel-workflow/</id>
        <link href="https://durable-workflow.com/uk/blog/saga-pattern-and-laravel-workflow/"/>
        <updated>2023-05-21T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Suppose we are working on a Laravel application that offers trip booking. A typical trip booking involves several steps such as:]]></summary>
        <content type="html"><![CDATA[<p>Suppose we are working on a Laravel application that offers trip booking. A typical trip booking involves several steps such as:</p>
<ol>
<li class="">Booking a flight.</li>
<li class="">Booking a hotel.</li>
<li class="">Booking a rental car.</li>
</ol>
<p>Our customers expect an all-or-nothing transaction — it doesn’t make sense to book a hotel without a flight. Now imagine each of these booking steps being represented by a distinct API.</p>
<p>Together, these steps form a distributed transaction spanning multiple services and databases. For a successful booking, all three APIs must accomplish their individual local transactions. If any step fails, the preceding successful transactions need to be reversed in an orderly fashion. With money and bookings at stake, we can’t merely erase prior transactions — we need an immutable record of attempts and failures. Thus, we should compile a list of compensatory actions for execution in the event of a failure.</p>
<p>To follow this tutorial, you should:</p>
<ol>
<li class="">Set up a local development environment for Workflow applications in PHP or use the sample app in a GitHub <a href="https://github.com/durable-workflow/sample-app/tree/Laravel-12" target="_blank" rel="noopener noreferrer" class="">codespace</a>.</li>
<li class="">Familiarize yourself with the basics of starting a Workflow project by reviewing the <a href="https://durable-workflow.com/docs/installation" target="_blank" rel="noopener noreferrer" class="">documentation</a>.</li>
<li class="">Review the <a href="https://microservices.io/patterns/data/saga.html" target="_blank" rel="noopener noreferrer" class="">Saga architecture pattern</a>.</li>
</ol>
<p>Sagas are an established design pattern for managing complex, long-running operations:</p>
<ol>
<li class="">A Saga manages transactions using a sequence of local transactions.</li>
<li class="">A local transaction is a work unit performed by a saga participant (a microservice).</li>
<li class="">Each operation in the Saga can be reversed by a compensatory transaction.</li>
<li class="">The Saga pattern assures that all operations are either completed successfully or the corresponding compensation transactions are run to reverse any completed work.</li>
</ol>
<p>Workflow (the Laravel-native durable workflow package) provides inherent support for the Saga pattern, simplifying the process of handling rollbacks and executing compensatory transactions.</p>
<h1>Booking Saga Flow</h1>
<p>We will visualize the Saga pattern for our trip booking scenario with a diagram.</p>
<div class="themedImageWrapper_nM_G"><div class="lightImage_srvP"><a href="https://mermaid.live/edit#pako:eNptkstuwjAQRX_FmrVBYEgwqdSKhNei7QJWLWFhJZMQ1djIcdRHxL83JA2kVb3y1Zw545FcQqRjBA8Sqd-jgzCWPG5CRaoz221tlfek17snfrmUWXqwZFtEEeZ5UsiHc6ga0q8Q8oJ5TQblWluUf8Ar9qxrar2bSUl8rd8yleYkECpCKTHet86g65yXG1RWyIoz_4iDjni1a1ykee_VN-_6Fr-n34z7DvwjXLbCeq2rb9lMa8Kq2amtRVLkOfFpQOckxijLM63uuqUZXdAlXdE1EZG9FIFCarIYPGsKpHBEcxSXCOWlLQR7wCOG4FXXGBNRSBtCqM5V20moV62PbafRRXoALxEyr1JxioXFeSZSI24IqhhNoAtlwXNqA3glfIA3dJ2-O-HOZMiZM5jygUvhEzzG3b47dlw-HbMBd5gzOlP4qocO-s54NGV8yoaMMZe7nALGmdXmqflW9e86fwOyNLgF" target="_blank" rel="noopener noreferrer"><img src="https://mermaid.ink/img/pako:eNptkstuwjAQRX_FmrVBYEgwqdSKhNei7QJWLWFhJZMQ1djIcdRHxL83JA2kVb3y1Zw545FcQqRjBA8Sqd-jgzCWPG5CRaoz221tlfek17snfrmUWXqwZFtEEeZ5UsiHc6ga0q8Q8oJ5TQblWluUf8Ar9qxrar2bSUl8rd8yleYkECpCKTHet86g65yXG1RWyIoz_4iDjni1a1ykee_VN-_6Fr-n34z7DvwjXLbCeq2rb9lMa8Kq2amtRVLkOfFpQOckxijLM63uuqUZXdAlXdE1EZG9FIFCarIYPGsKpHBEcxSXCOWlLQR7wCOG4FXXGBNRSBtCqM5V20moV62PbafRRXoALxEyr1JxioXFeSZSI24IqhhNoAtlwXNqA3glfIA3dJ2-O-HOZMiZM5jygUvhEzzG3b47dlw-HbMBd5gzOlP4qocO-s54NGV8yoaMMZe7nALGmdXmqflW9e86fwOyNLgF?type=png" alt="Saga Pattern Flow Diagram"></a></div><div class="darkImage_qlOL"><a href="https://mermaid.live/edit#pako:eNptkktvgzAMgP9K5HNatSnQwKRNBfo4bDu0p630EEEKqCGpQtAeiP8-CmvHpuUUx58_x5JriFXCwYOjUG9xxrRBj9tIovYs9jvTxgc0Gt0jv16JPM0M2lVxzMvyWImHJpI96bcIeuFlRwb1Rhku_oA37Fl11Ga_EAL5Sp1ymZYoYDLmQvDkcHUGQ2dYb7k0TLSc_kccDMTrfe9C_X9vvnDoW_7u_mM8DOBv4eoq7Ma6-VZ9tz5Y9zNdc7FgZYl8HOAQJTzOy1zJu2FqgZd4hdd4g1hsLknAkOo8Ac_oimMouC7YJYT6UhaByXjBI_Daa8L0KYJINm3NmclXpYprmVZVmoF3ZKJso-qcMMPDnKWaFbdXzWXCdaAqacCz7M4BXg3v4E0de-zMqT2fUmJPXDpxMHyAR6gzdizboa5FJtQm9qzB8Nm1nYxta-YS6pIpIcShDsXAk9wo_dRvVbdczRe1TrdB" target="_blank" rel="noopener noreferrer"><img src="https://mermaid.ink/img/pako:eNptkktvgzAMgP9K5HNatSnQwKRNBfo4bDu0p630EEEKqCGpQtAeiP8-CmvHpuUUx58_x5JriFXCwYOjUG9xxrRBj9tIovYs9jvTxgc0Gt0jv16JPM0M2lVxzMvyWImHJpI96bcIeuFlRwb1Rhku_oA37Fl11Ga_EAL5Sp1ymZYoYDLmQvDkcHUGQ2dYb7k0TLSc_kccDMTrfe9C_X9vvnDoW_7u_mM8DOBv4eoq7Ma6-VZ9tz5Y9zNdc7FgZYl8HOAQJTzOy1zJu2FqgZd4hdd4g1hsLknAkOo8Ac_oimMouC7YJYT6UhaByXjBI_Daa8L0KYJINm3NmclXpYprmVZVmoF3ZKJso-qcMMPDnKWaFbdXzWXCdaAqacCz7M4BXg3v4E0de-zMqT2fUmJPXDpxMHyAR6gzdizboa5FJtQm9qzB8Nm1nYxta-YS6pIpIcShDsXAk9wo_dRvVbdczRe1TrdB?type=png" alt="Saga Pattern Flow Diagram"></a></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="workflow-implementation">Workflow Implementation<a href="https://durable-workflow.com/uk/blog/saga-pattern-and-laravel-workflow/#workflow-implementation" class="hash-link" aria-label="Пряме посилання на Workflow Implementation" title="Пряме посилання на Workflow Implementation" translate="no">​</a></h2>
<p>We’ll begin by creating a high-level flow of our trip booking process, which we’ll name <code>BookingSagaWorkflow</code>.</p>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">class BookingSagaWorkflow extends Workflow  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function execute()  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<p>Next, we’ll imbue our saga with logic, by adding booking steps:</p>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">use function Workflow\activity;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Workflow\Workflow;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">class BookingSagaWorkflow extends Workflow  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function execute()  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        try {  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            $flightId = yield activity(BookFlightActivity::class);  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            $hotelId = yield activity(BookHotelActivity::class);  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            $carId = yield activity(BookRentalCarActivity::class);  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        } catch (Throwable $th) {  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        }  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<p>Everything inside the <code>try</code> block is our "happy path". If any steps within this distributed transaction fail, we move into the <code>catch</code> block and execute compensations.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="adding-compensations">Adding Compensations<a href="https://durable-workflow.com/uk/blog/saga-pattern-and-laravel-workflow/#adding-compensations" class="hash-link" aria-label="Пряме посилання на Adding Compensations" title="Пряме посилання на Adding Compensations" translate="no">​</a></h2>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">use function Workflow\activity;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Workflow\Workflow;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">class BookingSagaWorkflow extends Workflow  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function execute()  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        try {  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            $flightId = yield activity(BookFlightActivity::class);  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            $this-&gt;addCompensation(fn () =&gt; activity(CancelFlightActivity::class, $flightId));  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            $hotelId = yield activity(BookHotelActivity::class);  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            $this-&gt;addCompensation(fn () =&gt; activity(CancelHotelActivity::class, $hotelId));  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            $carId = yield activity(BookRentalCarActivity::class);  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            $this-&gt;addCompensation(fn () =&gt; activity(CancelRentalCarActivity::class, $carId));  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        } catch (Throwable $th) {  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        }  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<p>In the above code, we sequentially book a flight, a hotel, and a car. We use the <code>$this-&gt;addCompensation()</code> method to add a compensation, providing a callable to reverse a distributed transaction.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="executing-the-compensation-strategy">Executing the Compensation Strategy<a href="https://durable-workflow.com/uk/blog/saga-pattern-and-laravel-workflow/#executing-the-compensation-strategy" class="hash-link" aria-label="Пряме посилання на Executing the Compensation Strategy" title="Пряме посилання на Executing the Compensation Strategy" translate="no">​</a></h2>
<p>With the above setup, we can finalize our saga and populate the <code>catch</code> block:</p>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">class BookingSagaWorkflow extends Workflow  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function execute()  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        try {  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            $flightId = yield activity(BookFlightActivity::class);  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            $this-&gt;addCompensation(fn () =&gt; activity(CancelFlightActivity::class, $flightId));  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            $hotelId = yield activity(BookHotelActivity::class);  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            $this-&gt;addCompensation(fn () =&gt; activity(CancelHotelActivity::class, $hotelId));  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            $carId = yield activity(BookRentalCarActivity::class);  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            $this-&gt;addCompensation(fn () =&gt; activity(CancelRentalCarActivity::class, $carId));  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        } catch (Throwable $th) {  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            yield from $this-&gt;compensate();  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            throw $th;  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        }  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<p>Within the <code>catch</code> block, we call the <code>compensate()</code> method, which triggers the compensation strategy and executes all previously registered compensation callbacks. Once done, we rethrow the exception for debugging.</p>
<p>By default, compensations execute sequentially. To run them in parallel, use <code>$this-&gt;setParallelCompensation(true)</code>. To ignore exceptions that occur inside compensation activities while keeping them sequential, use <code>$this-&gt;setContinueWithError(true)</code> instead.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="testing-the-workflow">Testing the Workflow<a href="https://durable-workflow.com/uk/blog/saga-pattern-and-laravel-workflow/#testing-the-workflow" class="hash-link" aria-label="Пряме посилання на Testing the Workflow" title="Пряме посилання на Testing the Workflow" translate="no">​</a></h2>
<p>Let’s run this workflow with simulated failures in each activity to fully understand the process.</p>
<p>First, we run the workflow normally to see the sequence of bookings: flight, then hotel, then rental car.</p>
<p><img decoding="async" loading="lazy" src="https://miro.medium.com/v2/1*3IgEjKzHK8Fpp-uumr4dIw.png" alt="booking saga with no errors" class="img_ev3q"></p>
<p>Next, we simulate an error with the flight booking activity. Since no bookings were made, the workflow logs the exception and fails.</p>
<p><img decoding="async" loading="lazy" src="https://miro.medium.com/v2/1*ZuDAFa_q0l2-PT6PhRguaw.png" alt="booking saga error with flight" class="img_ev3q"></p>
<p>Then, we simulate an error with the hotel booking activity. The flight is booked successfully, but when the hotel booking fails, the workflow cancels the flight.</p>
<p><img decoding="async" loading="lazy" src="https://miro.medium.com/v2/1*_OwO5PUOLFqcLfd38gNpEQ.png" alt="booking saga error with hotel" class="img_ev3q"></p>
<p>Finally, we simulate an error with the rental car booking. The flight and hotel are booked successfully, but when the rental car booking fails, the workflow cancels the hotel first and then the flight.</p>
<p><img decoding="async" loading="lazy" src="https://miro.medium.com/v2/1*3qR9GKQH-YtghwPK_x9wUQ.png" alt="booking saga error with rental car" class="img_ev3q"></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="conclusion">Conclusion<a href="https://durable-workflow.com/uk/blog/saga-pattern-and-laravel-workflow/#conclusion" class="hash-link" aria-label="Пряме посилання на Conclusion" title="Пряме посилання на Conclusion" translate="no">​</a></h2>
<p>In this tutorial, we implemented the Saga architecture pattern for distributed transactions in a microservices-based application using Workflow. Writing Sagas can be complex, but Workflow takes care of the difficult parts such as handling errors and retries, and invoking compensatory transactions, allowing us to focus on the details of our application.</p>]]></content>
        <author>
            <name>Richard</name>
            <uri>https://github.com/rmcdaniel</uri>
        </author>
        <category label="sagas" term="sagas"/>
        <category label="microservices" term="microservices"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Laravel Workflow and State Machines with Finite]]></title>
        <id>https://durable-workflow.com/uk/blog/combining-laravel-workflow-and-state-machines/</id>
        <link href="https://durable-workflow.com/uk/blog/combining-laravel-workflow-and-state-machines/"/>
        <updated>2023-04-25T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Integrate Laravel Workflow with Finite to model a loan process with explicit created, submitted, approved, and denied transitions driven by workflow signals.]]></summary>
        <content type="html"><![CDATA[<p>Laravel Workflow handles queued, long-running execution, while Finite makes allowed state transitions explicit. This guide combines them in a loan-application workflow where signals move the process through created, submitted, approved, and denied states.</p>
<p>Using the Workflow package and a state machine together provides several advantages:</p>
<ol>
<li class="">Flexibility and modularity: Workflow allows developers to break down complex processes into smaller, modular units that are easy to maintain and update.</li>
<li class="">Explicit control over transitions: State machines provide a clear visualization of workflow states, activities, and transitions, making it easier to understand and maintain.</li>
<li class="">Robust error handling and retries: Workflow offers built-in support for handling errors and retries, ensuring that workflows are executed reliably and consistently.</li>
<li class="">Scalability: Workflow supports queuing and parallel execution, allowing workflows to be executed asynchronously on worker servers.</li>
<li class="">Integration with Laravel’s queue and event systems: This allows for seamless integration with other Laravel features and packages.</li>
</ol>
<h1>Installation Guide</h1>
<p>To get started with Workflow and Finite, you will need to install them in your Laravel project:</p>
<p>For Workflow, run the following command:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">composer require laravel-workflow/laravel-workflow</span><br></div></code></pre></div></div>
<p>For <a href="https://github.com/yohang/Finite" target="_blank" rel="noopener noreferrer" class="">Finite</a>, run the following command:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">composer require yohang/finite</span><br></div></code></pre></div></div>
<h1>Loan Application Workflow Example</h1>
<p>The following code demonstrates how to create a <code>LoanApplicationWorkflow</code> using Workflow and Finite:</p>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Finite\StatefulInterface;  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Finite\StateMachine\StateMachine;  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Finite\State\State;  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Finite\State\StateInterface;  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use function Workflow\await;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Workflow\Models\StoredWorkflow;  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Workflow\SignalMethod;  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Workflow\Workflow;  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">class LoanApplicationWorkflow extends Workflow implements StatefulInterface  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    private $state;  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    private $stateMachine;  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function setFiniteState($state)  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $this-&gt;state = $state;  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function getFiniteState()  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        return $this-&gt;state;  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    #[SignalMethod]  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function submit()  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $this-&gt;stateMachine-&gt;apply('submit');  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    #[SignalMethod]  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function approve()  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $this-&gt;stateMachine-&gt;apply('approve');  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    #[SignalMethod]  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function deny()  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $this-&gt;stateMachine-&gt;apply('deny');  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function isSubmitted()  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        return $this-&gt;stateMachine-&gt;getCurrentState()-&gt;getName() === 'submitted';  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function isApproved()  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        return $this-&gt;stateMachine-&gt;getCurrentState()-&gt;getName() === 'approved';  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function isDenied()  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        return $this-&gt;stateMachine-&gt;getCurrentState()-&gt;getName() === 'denied';  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function __construct(  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        public StoredWorkflow $storedWorkflow,  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        ...$arguments  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    ) {  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        parent::__construct($storedWorkflow, $arguments);  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $this-&gt;stateMachine = new StateMachine();  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $this-&gt;stateMachine-&gt;addState(new State('created', StateInterface::TYPE\_INITIAL));  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $this-&gt;stateMachine-&gt;addState('submitted');  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $this-&gt;stateMachine-&gt;addState(new State('approved', StateInterface::TYPE\_FINAL));  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $this-&gt;stateMachine-&gt;addState(new State('denied', StateInterface::TYPE\_FINAL));  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $this-&gt;stateMachine-&gt;addTransition('submit', 'created', 'submitted');  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $this-&gt;stateMachine-&gt;addTransition('approve', 'submitted', 'approved');  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $this-&gt;stateMachine-&gt;addTransition('deny', 'submitted', 'denied');  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $this-&gt;stateMachine-&gt;setObject($this);  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $this-&gt;stateMachine-&gt;initialize();  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function execute()  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        // loan created  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        yield await(fn () =&gt; $this-&gt;isSubmitted());  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        // loan submitted  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        yield await(fn () =&gt; $this-&gt;isApproved() || $this-&gt;isDenied());  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        // loan approved/denied  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        return $this-&gt;stateMachine-&gt;getCurrentState()-&gt;getName();  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<p>In this example, we define a <code>LoanApplicationWorkflow</code> class that extends <code>Workflow</code> and implements <code>StatefulInterface</code>. The workflow has four states: created, submitted, approved or denied. The workflow transitions between these states by externally calling the <code>submit()</code>, <code>approve()</code>, and <code>deny()</code> signal methods.</p>
<p>To use the <code>LoanApplicationWorkflow</code>, you can create a new instance of it, start the workflow, submit the loan application, approve it, and get the output as follows:</p>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">// create workflow  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">$workflow = WorkflowStub::make(LoanApplicationWorkflow::class);  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">// start workflow  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">$workflow-&gt;start();  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">sleep(1);  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">// submit signal  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">$workflow-&gt;submit();  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">sleep(1);  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">// approve signal  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">$workflow-&gt;approve();  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">sleep(1);  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">$workflow-&gt;output();  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">// "approved"</span><br></div></code></pre></div></div>
<p>This is the view from <a href="https://github.com/durable-workflow/waterline" target="_blank" rel="noopener noreferrer" class="">Waterline</a>.</p>
<p><img decoding="async" loading="lazy" src="https://miro.medium.com/max/1400/1*m6cOftX9kjBjr6CJGpyQPA.webp" alt="timeline" class="img_ev3q"></p>
<h1>Conclusion</h1>
<p>Although Workflow offers a way to define and manage workflows and activities, some developers might still prefer to use a state machine to have more explicit control over the transitions between states or activities.</p>
<p>A state machine can provide a more structured and visual representation of the workflow, making it easier to understand and maintain. In such cases, a state machine library can be integrated with Workflow. This allows developers to define their workflow states, activities, and transitions using the state machine library while still leveraging Workflow’s features, such as queuing, parallel execution, error handling, retries, and integration with Laravel’s queue and event systems.</p>
<p>The Laravel developer community has created several state machine packages that can be integrated with Workflow, such as the following:</p>
<ul>
<li class=""><a href="https://github.com/yohang/Finite" target="_blank" rel="noopener noreferrer" class="">https://github.com/yohang/Finite</a></li>
<li class=""><a href="https://github.com/spatie/laravel-model-states" target="_blank" rel="noopener noreferrer" class="">https://github.com/spatie/laravel-model-states</a></li>
<li class=""><a href="https://github.com/sebdesign/laravel-state-machine" target="_blank" rel="noopener noreferrer" class="">https://github.com/sebdesign/laravel-state-machine</a></li>
<li class=""><a href="https://github.com/symfony/workflow" target="_blank" rel="noopener noreferrer" class="">https://github.com/symfony/workflow</a></li>
</ul>
<p>By integrating a state machine library with Workflow, developers can get the best of both worlds: the flexibility and modularity of Workflow and the explicit control and visualization of a state machine. This can help to create more maintainable, robust, and scalable workflows for complex business processes.</p>]]></content>
        <author>
            <name>Richard</name>
            <uri>https://github.com/rmcdaniel</uri>
        </author>
        <category label="side-effects" term="side-effects"/>
        <category label="random" term="random"/>
        <category label="determinism" term="determinism"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Introducing Child Workflows in Workflow]]></title>
        <id>https://durable-workflow.com/uk/blog/introducing-child-workflows-in-laravel-workflow/</id>
        <link href="https://durable-workflow.com/uk/blog/introducing-child-workflows-in-laravel-workflow/"/>
        <updated>2023-04-05T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Workflow (the Laravel-native durable workflow package) has introduced an exciting new feature called “Child Workflows.” This addition aims to enhance the organization and maintainability of complex processes by allowing developers to encapsulate sub-processes within a parent workflow. This article will discuss the benefits of using child workflows, their similarities with running a workflow as an activity, and their compatibility with retry and resume features.]]></summary>
        <content type="html"><![CDATA[<p>Workflow (the Laravel-native durable workflow package) has introduced an exciting new feature called “Child Workflows.” This addition aims to enhance the organization and maintainability of complex processes by allowing developers to encapsulate sub-processes within a parent workflow. This article will discuss the benefits of using child workflows, their similarities with running a workflow as an activity, and their compatibility with retry and resume features.</p>
<p>In the Workflow package, child workflows are a way to manage complex processes by breaking them down into smaller, more manageable units. They enable developers to create hierarchical and modular structures for their workflows, making them more organized and easier to maintain. A child workflow is essentially a separate workflow that is invoked within a parent workflow using the <code>child()</code> helper function.</p>
<h1>Benefits of Using Child Workflows</h1>
<ol>
<li class="">Modularity: Child workflows promote modularity by allowing developers to encapsulate specific functionality within separate, reusable units. This enables better code organization and easier management of complex processes.</li>
<li class="">Reusability: Child workflows can be invoked within multiple parent workflows, which encourages reusability and reduces code duplication.</li>
<li class="">Maintainability: By breaking down complex processes into smaller units, developers can better understand, debug, and maintain their workflows.</li>
</ol>
<h1>Workflows as Activities</h1>
<p>Child workflows are similar to running a workflow as an activity in that they both encapsulate specific functionality within a parent workflow. However, child workflows offer more flexibility and reusability than activities.</p>
<div class="themedImageWrapper_nM_G"><div class="lightImage_srvP"><a href="https://mermaid.live/edit#pako:eNp1kl1rwjAUhv9KOOBdlTbaGnsx8Hu6DcYYDNbuImtTW2wTianOif99bYwoluUi5Jwn73tykhwhEjEDH5Jc7KOUSoXeRyFH1RgGr1QyrtCHkOsaf53zo2AYqWyXqQNyTGocLPhOrBkap1ke3wqMFWq3H9BIz-NLstVCzyxR6FtSHqW-0e6N1hhrySRo-NZwouHUwMahphrP7jE2eKbx_B53DZ5r_BhMeYxE8l9jVQ9v2Sq9NmGuLBJcZbxk29s2FkHjEAsNlkGj_FKDp0v55kuEHCxYySwGX8mSWVAwWdA6hGNtEYJKWcFC8KtlzBJa5iqEkJ8q2YbyTyGKi1KKcpWCn9B8W0XlJqaKTTK6kvS6hfGYybEouQLf6WoL8I_wU0We2_H6xO07BLv2gNieBQfwMfE6Xs_1yKCHbeJit3uy4FdXtTturzvAZIAdjLFHPGIBizMl5Mv5L-ovefoDcS7LIg" target="_blank" rel="noopener noreferrer"><img src="https://mermaid.ink/img/pako:eNp1kl1rwjAUhv9KOOBdlTbaGnsx8Hu6DcYYDNbuImtTW2wTianOif99bYwoluUi5Jwn73tykhwhEjEDH5Jc7KOUSoXeRyFH1RgGr1QyrtCHkOsaf53zo2AYqWyXqQNyTGocLPhOrBkap1ke3wqMFWq3H9BIz-NLstVCzyxR6FtSHqW-0e6N1hhrySRo-NZwouHUwMahphrP7jE2eKbx_B53DZ5r_BhMeYxE8l9jVQ9v2Sq9NmGuLBJcZbxk29s2FkHjEAsNlkGj_FKDp0v55kuEHCxYySwGX8mSWVAwWdA6hGNtEYJKWcFC8KtlzBJa5iqEkJ8q2YbyTyGKi1KKcpWCn9B8W0XlJqaKTTK6kvS6hfGYybEouQLf6WoL8I_wU0We2_H6xO07BLv2gNieBQfwMfE6Xs_1yKCHbeJit3uy4FdXtTturzvAZIAdjLFHPGIBizMl5Mv5L-ovefoDcS7LIg?type=png" alt="Child Workflows Diagram"></a></div><div class="darkImage_qlOL"><a href="https://mermaid.live/edit#pako:eNp1kl1rwjAUhv9KOOBdFRttjLkY-D3dBmMMBmt3kbXRFm0iMdU58b-vxriJZbkIOefhfc85SQ4Qq0QAg_lK7eKUa4Ne-5FE5eqFz1wLadCb0ssT_jjn-2EvNtk2M3vku9QgnMqtWgo0SLNVci1wVqhev0N9uw8uyVoNPYq5QZ-ayzhlTrtzWmdsJcOw4nuCQwtHDlaaGlk8vsXY4bHFk1vccnhi8X04kglS8_8GK2d4yRbp3xDuymIlTSYLsbkeYxpWmphaMAsr5WcWPFzKV18ikuDBQmcJMKML4UEudM5PIRxOFhGYVOQiAlYeE66XEUTyWGrWXL4rlV9kWhWLFNicrzZlVKwTbsQw4wvN899sWTsReqAKaYCRwHoAO8AXMJ8EDdKhQcenOGh2aZN4sAeGKWmQdkBot42bNMBB6-jBty3bbATtVhfTLvYxxoQS6oFIMqP00_kn2g95_AGd-cox" target="_blank" rel="noopener noreferrer"><img src="https://mermaid.ink/img/pako:eNp1kl1rwjAUhv9KOOBdFRttjLkY-D3dBmMMBmt3kbXRFm0iMdU58b-vxriJZbkIOefhfc85SQ4Qq0QAg_lK7eKUa4Ne-5FE5eqFz1wLadCb0ssT_jjn-2EvNtk2M3vku9QgnMqtWgo0SLNVci1wVqhev0N9uw8uyVoNPYq5QZ-ayzhlTrtzWmdsJcOw4nuCQwtHDlaaGlk8vsXY4bHFk1vccnhi8X04kglS8_8GK2d4yRbp3xDuymIlTSYLsbkeYxpWmphaMAsr5WcWPFzKV18ikuDBQmcJMKML4UEudM5PIRxOFhGYVOQiAlYeE66XEUTyWGrWXL4rlV9kWhWLFNicrzZlVKwTbsQw4wvN899sWTsReqAKaYCRwHoAO8AXMJ8EDdKhQcenOGh2aZN4sAeGKWmQdkBot42bNMBB6-jBty3bbATtVhfTLvYxxoQS6oFIMqP00_kn2g95_AGd-cox?type=png" alt="Child Workflows Diagram"></a></div></div>
<p>Activities are single-purpose units that perform a specific action within a workflow, such as sending an email or updating a database record. On the other hand, child workflows are complete workflows in themselves, which can be composed of multiple activities and even other child workflows. This allows developers to create complex, nested structures to manage intricate processes more efficiently.</p>
<h1>Retries and Resumes in Child Workflows</h1>
<p>Child workflows inherit the same retry and resume features as their parent workflows, enabling developers to manage error handling and recovery more effectively. When a child workflow fails, Workflow will automatically attempt to retry the failed operation, following the configured retry policy. If the child workflow still fails after all retries have been exhausted, the parent workflow can also be configured to handle the failure accordingly.</p>
<p>In addition, child workflows can be resumed if they are interrupted due to a system failure or crash. This ensures that the entire process can continue from the point of interruption without losing progress or requiring manual intervention.</p>
<h1>Conclusion</h1>
<p>Workflow’s Child Workflows feature offers developers an effective way to manage complex processes by breaking them down into smaller, more manageable units. This enhances organization, maintainability, and reusability, making it easier for developers to build and maintain intricate workflows. With the added benefits of retry and resume features, child workflows provide a robust and efficient solution for managing complex processes in Laravel applications.</p>]]></content>
        <author>
            <name>Richard</name>
            <uri>https://github.com/rmcdaniel</uri>
        </author>
        <category label="child-workflows" term="child-workflows"/>
        <category label="nesting" term="nesting"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[New Workflow Feature: Side Effects]]></title>
        <id>https://durable-workflow.com/uk/blog/new-laravel-workflow-feature-side-effects/</id>
        <link href="https://durable-workflow.com/uk/blog/new-laravel-workflow-feature-side-effects/"/>
        <updated>2022-12-22T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Workflows provide a more organized and structured approach to managing distributed processes, making it easier for developers to understand and work with complex logic.]]></summary>
        <content type="html"><![CDATA[<p>Workflows provide a more organized and structured approach to managing distributed processes, making it easier for developers to understand and work with complex logic.</p>
<p>Workflow is a powerful package for the Laravel web framework that provides tools for defining and managing workflows.</p>
<p>One of the key features of any workflow engine is the ability to track the history of a workflow as it is executed which allows a workflow to be retried if it fails or encounters an error. However, this also means that your workflow code must be deterministic and any non-deterministic code has to be carefully managed.</p>
<p>Recently, the Workflow package added support for <a href="https://durable-workflow.com/docs/features/side-effects" target="_blank" rel="noopener noreferrer" class="">side effects</a>, which are closures containing non-deterministic code that is only executed once and the result saved. Side effects are a useful way to introduce non-deterministic behavior into a workflow, such as generating a random number or UUID.</p>
<p>Here is an example workflow that demonstrates side effects.</p>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Workflow\Workflow;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use function Workflow\{activity, sideEffect};</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">class SideEffectWorkflow extends Workflow  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function execute()  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $sideEffect = yield sideEffect(  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">          fn () =&gt; random_int(PHP_INT_MIN, PHP_INT_MAX)  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        );  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $badSideEffect = random_int(PHP_INT_MIN, PHP_INT_MAX);  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $result1 = yield activity(SimpleActivity::class, $sideEffect);  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $result2 = yield activity(SimpleActivity::class, $badSideEffect);  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        if ($sideEffect !== $result1) {  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            throw new Exception(  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                'These side effects should match because it was properly wrapped in sideEffect().'  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            );  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        }  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        if ($badSideEffect === $result2) {  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            throw new Exception(  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                'These side effects should not match because it was not wrapped in sideEffect().'  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            );  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        }  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<p>The activity doesn’t actually do anything. It just takes the input and passes it back out unmodified, so that we can compare the result to what we generated inside of the workflow.</p>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">class SimpleActivity extends Activity  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function execute($input)  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        return $input;  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<p>In this example, the workflow generates two random integers: one using a side effect and the other using a local variable. The values of these integers are then passed to two different activities.</p>
<p>The first activity receives the value of the side effect, which has been saved. As a result, the value of the side effect should remain constant throughout the execution of the workflow.</p>
<p>The second activity receives the value of the local variable, which is not saved and will be regenerated. This means that the value of the local variable will change between executions of the workflow.</p>
<p>As a result, it is not expected that the value of the local variable will match the value returned from the second activity. The odds of two random integers generated using <code>random_int(PHP_INT_MIN, PHP_INT_MAX)</code> being equal are extremely low, since there are a very large number of possible integers in this range.</p>
<p>It’s important to use side effects appropriately in your workflow to ensure that your workflow is reliable and can recover from failures. Only use side effects for short pieces of code that cannot fail, and make sure to use activities to perform long-running work that may fail and need to be retried, such as API requests or external processes.</p>
<p>Overall, side effects are a powerful tool for introducing non-deterministic behavior into your workflows. When used correctly, they can help you to add more flexibility and complexity to your application’s logic.</p>
<p>Workflow is a powerful tool for managing workflows in your Laravel applications, and the addition of support for side effects makes it even more powerful!</p>]]></content>
        <author>
            <name>Richard</name>
            <uri>https://github.com/rmcdaniel</uri>
        </author>
        <category label="side-effects" term="side-effects"/>
        <category label="random" term="random"/>
        <category label="determinism" term="determinism"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Laravel Job Chaining vs. Fan-Out/Fan-In]]></title>
        <id>https://durable-workflow.com/uk/blog/job-chaining-vs-fan-out-fan-in/</id>
        <link href="https://durable-workflow.com/uk/blog/job-chaining-vs-fan-out-fan-in/"/>
        <updated>2022-12-06T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Compare sequential Laravel job chaining with parallel fan-out/fan-in in a Workflow example that converts PDF pages concurrently, then merges the results.]]></summary>
        <content type="html"><![CDATA[<p><a href="https://laravel.com/docs/9.x/queues#job-chaining" target="_blank" rel="noopener noreferrer" class="">Laravel job chaining</a> fits work that must run step by step, with one activity's output feeding the next. When independent activities can run at the same time and their outputs must be recombined, fan-out/fan-in is the better fit. This guide compares both patterns, then builds a Workflow PDF pipeline that converts pages concurrently before merging them.</p>
<div class="themedImageWrapper_nM_G" data-diagram-id="job-chaining"><div class="lightImage_srvP"><img src="https://durable-workflow.com/img/job-chaining/job-chaining-light.svg" alt="Three functions chained sequentially through two database steps"></div><div class="darkImage_qlOL"><img src="https://durable-workflow.com/img/job-chaining/job-chaining-dark.svg" alt="Three functions chained sequentially through two database steps"></div></div>
<p>In contrast, the fan-out/fan-in pattern involves dividing a task into smaller sub-tasks and then combining the results of those sub-tasks to produce the final result. This pattern is often used to parallelize a task and improve its performance by leveraging the power of multiple queue workers.</p>
<div class="themedImageWrapper_nM_G" data-diagram-id="fan-out-fan-in"><div class="lightImage_srvP"><img src="https://durable-workflow.com/img/job-chaining/fan-out-fan-in-light.svg" alt="One function fanning out to two parallel functions, then joining through a database before the final function"></div><div class="darkImage_qlOL"><img src="https://durable-workflow.com/img/job-chaining/fan-out-fan-in-dark.svg" alt="One function fanning out to two parallel functions, then joining through a database before the final function"></div></div>
<p>There are two phases: fan-out and fan-in.</p>
<p>In the fan-out phase, the workflow divides the main task into smaller sub-tasks and assigns each of those sub-tasks to a different activity. In the fan-in phase, the workflow collects the results of the activities and combines them to produce the final result.</p>
<p>The below workflow represents a simple example of a fan-out/fan-in pattern in which multiple activities are executed in parallel and their results are then merged together.</p>
<p>The workflow divides the task of creating a PDF into activities, with each activity responsible for rendering a single page of the document. Once the individual pages have been rendered, the fan-in phase of the workflow combines the rendered pages into a single PDF document.</p>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">namespace App\Workflows\BuildPDF;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Workflow\Workflow;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use function Workflow\{activity, all};</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">class BuildPDFWorkflow extends Workflow</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function execute()</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $page1 = activity(ConvertURLActivity::class, 'https://example.com/');</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $page2 = activity(ConvertURLActivity::class, 'https://example.com/');</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $pages = yield all([$page1, $page2]);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $result = yield activity(MergePDFActivity::class, $pages);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        return $result;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<p>The <code>ConvertURLActivity</code> is passed a URL as an argument, and it converts the contents of that URL into a PDF document. Because two separate activities are created, this results in the execution of two instances of <code>ConvertURLActivity</code> in parallel.</p>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">namespace App\Workflows\BuildPDF;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Illuminate\Support\Facades\Http;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Workflow\Activity;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">class ConvertURLActivity extends Activity</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function execute($url)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $fileName = uniqid() . '.pdf';</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        Http::withHeaders([</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'Apikey' =&gt; 'YOUR-API-KEY-GOES-HERE',</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        ])</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        -&gt;withOptions([</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'sink' =&gt; storage_path($fileName),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        ])</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        -&gt;post('https://api.cloudmersive.com/convert/web/url/to/pdf', [</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'Url' =&gt; $url,</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        ]);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        return $fileName;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<p>Next, the <code>BuildPDFWorkflow</code> uses <code>all()</code> to wait for both <code>ConvertURLActivity</code> instances to complete. This is an example of the fan-in part of the fan-out/fan-in pattern, as it collects the results of the parallel activities and combines them into a single array of PDF files.</p>
<p>Finally, the <code>BuildPDFWorkflow</code> executes the<code>MergePDFActivity</code>, which is passed the array of PDFs that were generated by the <code>ConvertURLActivity</code> instances, and merges them into a single PDF document.</p>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">namespace App\Workflows\BuildPDF;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use setasign\Fpdi\Fpdi;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Workflow\Activity;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">class MergePDFActivity extends Activity</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function execute($pages)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $fileName = uniqid() . '.pdf';</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $pdf = new Fpdi();</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        foreach ($pages as $page) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            $pdf-&gt;AddPage();</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            $pdf-&gt;setSourceFile(storage_path($page));</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            $pdf-&gt;useTemplate($pdf-&gt;importPage(1));</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $pdf-&gt;Output('F', storage_path($fileName));</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        foreach ($pages as $page) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            unlink(storage_path($page));</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        return $fileName;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<p>This is what the final PDF looks like…</p>
<p><img decoding="async" loading="lazy" src="https://miro.medium.com/max/1400/1*A3PKGEk8JptFIxB9IqCh6w.webp" alt="merged PDF" class="img_ev3q"></p>
<p>Overall, using the fan-out/fan-in pattern in this way can significantly reduce the time it takes to create a PDF document, making the process more efficient and scalable.</p>
<p>Thanks for reading!</p>]]></content>
        <author>
            <name>Richard</name>
            <uri>https://github.com/rmcdaniel</uri>
        </author>
        <category label="chaining" term="chaining"/>
        <category label="fan-out" term="fan-out"/>
        <category label="fan-in" term="fan-in"/>
        <category label="batching" term="batching"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Waterline: Elegant UI for Workflows]]></title>
        <id>https://durable-workflow.com/uk/blog/waterline-ui/</id>
        <link href="https://durable-workflow.com/uk/blog/waterline-ui/"/>
        <updated>2022-11-19T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[This post was written for Durable Workflow v1. For v2, the package name has changed from durable-workflow/waterline to durable-workflow/waterline. See the migration guide for details.]]></summary>
        <content type="html"><![CDATA[<div class="theme-admonition theme-admonition-info admonition_xJq3 alert alert--info"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>V2 Update</div><div class="admonitionContent_BuS1"><p>This post was written for Durable Workflow v1. For v2, the package name has changed from <code>durable-workflow/waterline</code> to <code>durable-workflow/waterline</code>. See the <a class="" href="https://durable-workflow.com/uk/docs/migration/">migration guide</a> for details.</p></div></div>
<p>One of the pros to using workflows is that it makes monitoring easy. Using Waterline makes it even easier!</p>
<p><img decoding="async" loading="lazy" src="https://miro.medium.com/max/1400/1*2FP4crjpM8C48kAnqAjv5A.webp" alt="dashboard" class="img_ev3q"></p>
<p>Look familiar? Yes, this is shamelessly based on Horizon! However, the similarity is only superficial. Waterline is geared towards workflows, not queues. In fact, Horizon is still the best way to monitor your queues and plays along nicely with it.</p>
<blockquote>
<p>Waterline is to workflows what Horizon is to queues.</p>
</blockquote>
<p><img decoding="async" loading="lazy" src="https://miro.medium.com/max/1400/1*EKWNNFy6kYRrqMbaozA8IQ.webp" alt="workflow view" class="img_ev3q"></p>
<p>At this point you can see a lot of differences! You can see the arguments passed to the workflow and the output from the completed workflow. You can see a timeline that shows each activity at a glance along with any exceptions that were thrown. There is also a list view for the activities and their results.</p>
<p>At the bottom are any exceptions thrown, including a stack trace and a snippet of code showing the exact line. This makes debugging a breeze.</p>
<p>If you’re familiar with Horizon then installing Waterline will be like déjà vu but the setup is simpler because Waterline doesn’t care about queues, only workflows.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="installation">Installation<a href="https://durable-workflow.com/uk/blog/waterline-ui/#installation" class="hash-link" aria-label="Пряме посилання на Installation" title="Пряме посилання на Installation" translate="no">​</a></h2>
<p>You can find the official <a href="https://github.com/durable-workflow/waterline" target="_blank" rel="noopener noreferrer" class="">documentation</a> here but setup is simple.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">composer require durable-workflow/waterline  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">php artisan waterline:publish</span><br></div></code></pre></div></div>
<p>That’s it! Now you should be able to view the <code>/waterline</code> URL in your app. By default this URL is only available in local environments. To view this outside of local environments you will have to modify the <code>WaterlineServiceProvider</code>.</p>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">Gate::define('viewWaterline', function ($user) {  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    return in_array($user-&gt;email, [  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        'admin@example.com',  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    ]);  </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">});</span><br></div></code></pre></div></div>
<p>This will allow only the single admin user to access the Waterline UI.</p>
<p>If you want more context for the workflow that is show in the screenshot above, make sure to read my <a href="https://medium.com/@laravel-workflow/email-verifications-using-laravel-workflow-acd6707aa7b3" target="_blank" rel="noopener noreferrer" class="">previous article</a>.</p>
<p>Thanks for reading!</p>]]></content>
        <author>
            <name>Richard</name>
            <uri>https://github.com/rmcdaniel</uri>
        </author>
        <category label="ui" term="ui"/>
        <category label="horizon" term="horizon"/>
        <category label="queues" term="queues"/>
        <category label="workflows" term="workflows"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Invalidating Cloud Images in Laravel with Workflows]]></title>
        <id>https://durable-workflow.com/uk/blog/invalidating-cloud-images/</id>
        <link href="https://durable-workflow.com/uk/blog/invalidating-cloud-images/"/>
        <updated>2022-11-15T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Many services like Cloud Image offer a way to invalidate cached images so that they are pulled from your server again. This is useful if you have updated the source image on your server and want future requests to use the latest copy.]]></summary>
        <content type="html"><![CDATA[<p>Many services like <a href="https://docs.cloudimage.io/go/cloudimage-documentation-v7/en/caching-acceleration/invalidation-api" target="_blank" rel="noopener noreferrer" class="">Cloud Image</a> offer a way to invalidate cached images so that they are pulled from your server again. This is useful if you have updated the source image on your server and want future requests to use the latest copy.</p>
<p>However, it can be challenging if you want to automate this and also ensure that the image has been invalidated. This is because most invalidation APIs are asynchronous. When you request an image to be cleared from the cache, the API will return a response immediately. Then the actual process to clear the image from the cache runs in the background, sometimes taking up to 30 seconds before the image is updated. You could simply trust that the process works but it is also possible to be 100% sure with an automated workflow.</p>
<p>The workflow we need to write is as follows:</p>
<ol>
<li class="">Check the currently cached image’s timestamp via HEAD call</li>
<li class="">Invalidate cached image via API call</li>
<li class="">Check if the image timestamp has changed</li>
<li class="">If not, wait a while and check again</li>
<li class="">After 3 failed checks, go back to step 2</li>
</ol>
<p>The workflow consists of two activities. The first activity gets the current timestamp of the image. This timestamp is used to determine if the image was actually cleared from the cache or not.</p>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">namespace App\Workflows\InvalidateCache;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Illuminate\Support\Facades\Http;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Workflow\Activity;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">class CheckImageDateActivity extends Activity</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function execute($url)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        return Http::head('https://' . config('services.cloudimage.token') . '.cloudimg.io/' . $url)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            -&gt;header('date');</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<p>The second activity makes the actual call to Cloud Image’s API to invalidate the image from the cache.</p>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">namespace App\Workflows\InvalidateCache;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Illuminate\Support\Facades\Http;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Workflow\Activity;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">class InvalidateCacheActivity extends Activity</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function execute($url)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        Http::withHeaders([</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'X-Client-key' =&gt; config('services.cloudimage.key'),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'Content-Type' =&gt; 'application/json'</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        ])-&gt;post('https://api.cloudimage.com/invalidate', [</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'scope' =&gt; 'original',</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            'urls' =&gt; [</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                '/' . $url</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            ],</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        ]);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<p>The workflow looks as follows and is the same process as outlined before.</p>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">namespace App\Workflows\InvalidateCache;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Workflow\Workflow;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use function Workflow\{activity, timer};</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">class InvalidateCacheWorkflow extends Workflow</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function execute($url)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $oldDate = yield activity(CheckImageDateActivity::class, $url);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        while (true) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            yield activity(InvalidateCacheActivity::class, $url);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            for ($i = 0; $i &lt; 3; ++$i) { </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                yield timer(30);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                $newDate = yield activity(CheckImageDateActivity::class, $url);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                if ($oldDate !== $newDate) return;    </span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<p>Line 13 uses an activity to get the current timestamp of the image we want to invalidate from the cache.</p>
<p>Line 15 starts a loop that only exits when the image timestamp has changed.</p>
<p>Line 16 uses an activity to invalidate the image from the cache.</p>
<p>Line 18 starts a loop that tries a maximum of three times to first sleep and then check if the image timestamp has change, after three times the loop restarts at line 15.</p>
<p>Line 19 sleeps the workflow for 30 seconds. This gives Cloud Image time to clear the image from their cache before checking the timestamp again.</p>
<p>Lines 21–23 reuse the activity from earlier to get the current timestamp of the cached image and compare it to the one saved on line 13. If the timestamps don’t match then the image has successfully been cleared from the cache and we can exit the workflow. Otherwise, after three attempts, we start the process over again.</p>
<p>This is how the workflow execution looks in the queue assuming no retries are needed.</p>
<p><img decoding="async" loading="lazy" src="https://miro.medium.com/max/1400/1*7psZLD9mKGJnzEw508oIAw.webp" alt="workflow execution" class="img_ev3q"></p>
<p>The added benefit is that your image is now cached again and will be fast for the next user! Thanks for reading!</p>]]></content>
        <author>
            <name>Richard</name>
            <uri>https://github.com/rmcdaniel</uri>
        </author>
        <category label="cache" term="cache"/>
        <category label="invalidation" term="invalidation"/>
        <category label="cloud" term="cloud"/>
        <category label="images" term="images"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Converting Videos with FFmpeg and Workflow]]></title>
        <id>https://durable-workflow.com/uk/blog/converting-videos-with-ffmpeg/</id>
        <link href="https://durable-workflow.com/uk/blog/converting-videos-with-ffmpeg/"/>
        <updated>2022-10-31T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[FFmpeg is a free, open-source software project allowing you to record, convert and stream audio and video.]]></summary>
        <content type="html"><![CDATA[<p><a href="https://ffmpeg.org/" target="_blank" rel="noopener noreferrer" class="">FFmpeg</a> is a free, open-source software project allowing you to record, convert and stream audio and video.</p>
<p><a href="https://laravel.com/docs/9.x/queues" target="_blank" rel="noopener noreferrer" class="">Laravel Queues</a> are great for long running tasks. Converting video takes a long time! With <a href="https://github.com/durable-workflow/workflow" target="_blank" rel="noopener noreferrer" class="">Workflow</a>, you can harness the power of queues to convert videos in the background and easily manage the process.</p>
<ol>
<li class="">You’ll need to <a href="https://ffmpeg.org/download.html" target="_blank" rel="noopener noreferrer" class="">install FFmpeg</a></li>
<li class="">Then <code>composer require php-ffmpeg/php-ffmpeg</code> (<a href="https://github.com/PHP-FFMpeg/PHP-FFMpeg#readme" target="_blank" rel="noopener noreferrer" class="">docs</a>)</li>
<li class="">Finally <code>composer require laravel-workflow/laravel-workflow</code> (<a href="https://github.com/durable-workflow/workflow" target="_blank" rel="noopener noreferrer" class="">docs</a>)</li>
</ol>
<h1>Workflow</h1>
<p>A workflow is an easy way to orchestrate activities. A workflow that converts a video from one format to another might have several activities, such as downloading the video from storage, the actual conversion, and then finally notifying the user that it’s finished.</p>
<p>For simplicity, the workflow we are making today will only contain the most interesting activity, converting the video.</p>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">namespace App\Workflows\ConvertVideo;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use function Workflow\activity;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Workflow\Workflow;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">class ConvertVideoWorkflow extends Workflow</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function execute()</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        yield activity(</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            ConvertVideoWebmActivity::class,</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            storage_path('app/oceans.mp4'),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            storage_path('app/oceans.webm'),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        );</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<p>We need a video to convert. We can use this one:</p>
<p><a href="http://vjs.zencdn.net/v/oceans.mp4" target="_blank" rel="noopener noreferrer" class="">http://vjs.zencdn.net/v/oceans.mp4</a></p>
<p>Download it and save it to your app storage folder.</p>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">namespace App\Workflows\ConvertVideo;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use FFMpeg\FFMpeg;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use FFMpeg\Format\Video\WebM;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Workflow\Activity;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">class ConvertVideoWebmActivity extends Activity</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public $timeout = 5;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function execute($input, $output)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $ffmpeg = FFMpeg::create();</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $video = $ffmpeg-&gt;open($input);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $format = new WebM();</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $format-&gt;on('progress', fn () =&gt; $this-&gt;heartbeat());</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $video-&gt;save($format, $output);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<p>The activity converts any input video into a <a href="https://www.webmproject.org/" target="_blank" rel="noopener noreferrer" class="">WebM</a> output video. While ffmpeg is converting the video, a progress callback is triggered which in turn heartbeats the activity.</p>
<p>This is necessary because we have set a reasonable timeout of 5 seconds but we also have no idea how long it will take to convert the video. As long as we send a heartbeat at least once every 5 seconds, the activity will not timeout.</p>
<p><img decoding="async" loading="lazy" src="https://miro.medium.com/max/1400/1*ccrxeOEZYQciDYEprRKWiQ.webp" alt="heartbeat" class="img_ev3q"></p>
<p><img decoding="async" loading="lazy" src="https://miro.medium.com/max/1400/1*9ZF3LTqjf4qsVcNVX5LK0A.webp" alt="no heartbeat" class="img_ev3q"></p>
<p>Without a heartbeat, the worker will be killed after the timeout of 5 seconds is reached.</p>
<p>To actually run the workflow you just need to call:</p>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">WorkflowStub::make(ConvertVideoWorkflow::class)-&gt;start();</span><br></div></code></pre></div></div>
<p>And that’s it!</p>]]></content>
        <author>
            <name>Richard</name>
            <uri>https://github.com/rmcdaniel</uri>
        </author>
        <category label="video" term="video"/>
        <category label="ffmpeg" term="ffmpeg"/>
        <category label="conversion" term="conversion"/>
        <category label="transcoding" term="transcoding"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Email Verifications Using Workflow]]></title>
        <id>https://durable-workflow.com/uk/blog/email-verifications/</id>
        <link href="https://durable-workflow.com/uk/blog/email-verifications/"/>
        <updated>2022-10-29T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[A typical registration process goes as follows:]]></summary>
        <content type="html"><![CDATA[<p>A typical registration process goes as follows:</p>
<ol>
<li class="">User fills out registration form and submits it</li>
<li class="">Laravel creates user in database with null <code>email_verified_at</code></li>
<li class="">Laravel sends email with a code, or a link back to our website</li>
<li class="">User enters code, or clicks link</li>
<li class="">Laravel sets <code>email_verified_at</code> to the current time</li>
</ol>
<p>What’s wrong with this? Nothing. But like all things, as soon as real world complexity creeps in, this pattern could become painful. What if you wanted to send an email after the code or link expires? And do you really need a user in your database if they never verify their email address?</p>
<p>Let’s take this trivial example and replace it with a workflow. This is based on the <a href="https://github.com/durable-workflow/workflow" target="_blank" rel="noopener noreferrer" class="">Workflow library</a> (the Laravel-native durable workflow package).</p>
<p>Create a standard Laravel application and create the following files. First, the API routes.</p>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">use App\Workflows\VerifyEmail\VerifyEmailWorkflow;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Illuminate\Support\Facades\Hash;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Illuminate\Support\Facades\Route;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Workflow\WorkflowStub;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">Route::get('/register', function () {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    $workflow = WorkflowStub::make(VerifyEmailWorkflow::class);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    $workflow-&gt;start(</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        'test+1@example.com',</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        Hash::make('password'),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    );</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    return response()-&gt;json([</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        'workflow_id' =&gt; $workflow-&gt;id(),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    ]);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">});</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">Route::get('/verify-email', function () {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    $workflow = WorkflowStub::load(request('workflow_id'));</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    $workflow-&gt;verify();</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    return response()-&gt;json('ok');</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">})-&gt;name('verify-email');</span><br></div></code></pre></div></div>
<p>The <code>register</code> route creates a new <code>VerifyEmailWorkflow</code> , passes in the email and password, and then starts the workflow. Notice that we hash the password before giving it to the workflow. This prevents the plain text from being stored in the workflow logs.</p>
<p>The <code>verify-email</code> route receives a workflow id, loads it and then calls the <code>verify()</code> signal method.</p>
<p>Now let’s take a look at the actual workflow.</p>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Workflow\SignalMethod;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Workflow\Workflow;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use function Workflow\{activity, await};</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">class VerifyEmailWorkflow extends Workflow</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    private bool $verified = false;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    #[SignalMethod]</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function verify()</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $this-&gt;verified = true;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function execute($email = '', $password = '')</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        yield activity(SendEmailVerificationEmailActivity::class, $email);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        yield await(fn () =&gt; $this-&gt;verified);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        yield activity(VerifyEmailActivity::class, $email, $password);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<p>Take notice of the <code>yield</code> keywords. Because PHP (and most other languages) cannot save their execution state, coroutines rather than normal functions are used inside of workflows (but not activities). A coroutine will be called multiple times in order to execute to completion.</p>
<table><thead><tr><th>Normal Function</th><th>Coroutine</th></tr></thead><tbody><tr><td><div class="themedImageWrapper_nM_G"><div class="lightImage_srvP"><a href="https://mermaid.live/edit#pako:eNplkU1vwyAMhv9K5HNaEUIH4bBLq562204LO9DgNJESqChoH1X--0i6al8-2a_fxwb5Ao0zCBLawb02nfYhe9opm6U4x8PR61OXPdB6H20Temdfrq059oWuPYbobyJao-xftKi3ehjQ_wC3CWyS-Es61An_Pyh5s9Xqfl52FfZfQiKUhRyOvjcgg4-Yw4h-1HMJl9msIHQ4ogKZUoOtjkNQoOyUsJO2z86NN9K7eOxAtno4pyqejA6463X6w7clvQr91kUbQBblMgLkBd5AUlKsBeGkEISSigqWwzvIkpG12PDNpqyIYFSUUw4fy06yZpwKzitWMX5HGSU8BzR9cP7xeozlJtMnm-Z59g" target="_blank" rel="noopener noreferrer"><img src="https://mermaid.ink/img/pako:eNplkU1vwyAMhv9K5HNaEUIH4bBLq562204LO9DgNJESqChoH1X--0i6al8-2a_fxwb5Ao0zCBLawb02nfYhe9opm6U4x8PR61OXPdB6H20Temdfrq059oWuPYbobyJao-xftKi3ehjQ_wC3CWyS-Es61An_Pyh5s9Xqfl52FfZfQiKUhRyOvjcgg4-Yw4h-1HMJl9msIHQ4ogKZUoOtjkNQoOyUsJO2z86NN9K7eOxAtno4pyqejA6463X6w7clvQr91kUbQBblMgLkBd5AUlKsBeGkEISSigqWwzvIkpG12PDNpqyIYFSUUw4fy06yZpwKzitWMX5HGSU8BzR9cP7xeozlJtMnm-Z59g?type=png" alt="Normal Function Diagram"></a></div><div class="darkImage_qlOL"><a href="https://mermaid.live/edit#pako:eNplkTtvwyAQgP-KdbNjYYIDZuiSKFO7darpQGxiW7UhuoD6sPLfi52m6uMm7uO-O9BNULvGgITj4F7rTqNPHnfKJjHO4dCiPnXJPa32wda-d_b5ejXHPtcVGh_wBo1tlP2r5tVWD4PBH-I2inWEv9Chivr_RrE2Wa3u5mFXsP8C0VAWUmixb0B6DCaF0eCo5xSmuViB78xoFMh4bDS-KFD2Ep2Ttk_OjTcNXWg7kEc9nGMWTo32Ztfr-IHxm2J8k8GtC9aD5BuxNAE5wRtISvJMEE5yQSgpqWApvINcM5KJghfFuiSCUbG-pPCxTCUZ41RwXrKS8Q1llPAUTNN7hw_XXSwruXwC4h15Pw" target="_blank" rel="noopener noreferrer"><img src="https://mermaid.ink/img/pako:eNplkTtvwyAQgP-KdbNjYYIDZuiSKFO7darpQGxiW7UhuoD6sPLfi52m6uMm7uO-O9BNULvGgITj4F7rTqNPHnfKJjHO4dCiPnXJPa32wda-d_b5ejXHPtcVGh_wBo1tlP2r5tVWD4PBH-I2inWEv9Chivr_RrE2Wa3u5mFXsP8C0VAWUmixb0B6DCaF0eCo5xSmuViB78xoFMh4bDS-KFD2Ep2Ttk_OjTcNXWg7kEc9nGMWTo32Ztfr-IHxm2J8k8GtC9aD5BuxNAE5wRtISvJMEE5yQSgpqWApvINcM5KJghfFuiSCUbG-pPCxTCUZ41RwXrKS8Q1llPAUTNN7hw_XXSwruXwC4h15Pw?type=png" alt="Normal Function Diagram"></a></div></div></td><td><div class="themedImageWrapper_nM_G"><div class="lightImage_srvP"><a href="https://mermaid.live/edit#pako:eNptkj1vwyAQhv-KdbMTYSAFM3RxtqhL1al2B2JIbMkGC4P6EeW_l9hOm1a5iXuf--BOd4LaKg0CDp19rxvpfPKyrUwSbQz7o5NDkzzjsrDOBt8a_Tazi-2ycgzjoI26FfE9kZRO--DMokVamf9NsrKQXafdTV6BZVlH8Y-0j7XG0Os_Yn1PVKWyPz--6RnLJqvVYxxg9nfZ5MbSV76fOV44Xnh95fXMycLJwmMHSOHoWgXCu6BT6LXr5cWF0yW2At_oXlcg4lPpgwydr6Ay55g2SPNqbX_NjPs-NiAOshujFwYlvd62Mi7rNyTOpF1hg_EgMjKVAHGCDxAYZWuOGMo4wijHnKbwCYJQtOYbttmQHHGKOTmn8DX1RGvKMGcspzllD5hixFLQqvXWPc33MZ3J-RssOJ-L" target="_blank" rel="noopener noreferrer"><img src="https://mermaid.ink/img/pako:eNptkj1vwyAQhv-KdbMTYSAFM3RxtqhL1al2B2JIbMkGC4P6EeW_l9hOm1a5iXuf--BOd4LaKg0CDp19rxvpfPKyrUwSbQz7o5NDkzzjsrDOBt8a_Tazi-2ycgzjoI26FfE9kZRO--DMokVamf9NsrKQXafdTV6BZVlH8Y-0j7XG0Os_Yn1PVKWyPz--6RnLJqvVYxxg9nfZ5MbSV76fOV44Xnh95fXMycLJwmMHSOHoWgXCu6BT6LXr5cWF0yW2At_oXlcg4lPpgwydr6Ay55g2SPNqbX_NjPs-NiAOshujFwYlvd62Mi7rNyTOpF1hg_EgMjKVAHGCDxAYZWuOGMo4wijHnKbwCYJQtOYbttmQHHGKOTmn8DX1RGvKMGcspzllD5hixFLQqvXWPc33MZ3J-RssOJ-L?type=png" alt="Coroutine Diagram"></a></div><div class="darkImage_qlOL"><a href="https://mermaid.live/edit#pako:eNptkj1vwyAQhv-KdbMTYUwKZujibFGXqlPtDsQQ26oNFgb1I8p_L7GdKKlyE_c-3HscuiNURirgcOjMV9UI66K3bamjEKPf11YMTfSKi9xY412r1cfMzrFLitGPg9LyVsSPxLSwynmrFy3QUv9vkhS56Dplb-pyLIoqiHfSPniNvld3YvVIlIU01xff9Ay20Wr1HAaY810ypcH6wvczxwvHC68uvJp5uvB04aEDxFDbVgJ31qsYemV7cU7heL5bgmtUr0rg4SiF_Syh1KdQMwj9bkx_KQufXTfAD6IbQ-YHKZzatiL8VH9VbZhI2dx47YDTBE8mwI_wDRyjZM0QRQlDGGWYkRh-gKcErdmGbjZphhjBLD3F8Dt1RWtCMaM0IxmhT5hgRGNQsnXGvszrMW3J6Q9Ifp7J" target="_blank" rel="noopener noreferrer"><img src="https://mermaid.ink/img/pako:eNptkj1vwyAQhv-KdbMTYUwKZujibFGXqlPtDsQQ26oNFgb1I8p_L7GdKKlyE_c-3HscuiNURirgcOjMV9UI66K3bamjEKPf11YMTfSKi9xY412r1cfMzrFLitGPg9LyVsSPxLSwynmrFy3QUv9vkhS56Dplb-pyLIoqiHfSPniNvld3YvVIlIU01xff9Ay20Wr1HAaY810ypcH6wvczxwvHC68uvJp5uvB04aEDxFDbVgJ31qsYemV7cU7heL5bgmtUr0rg4SiF_Syh1KdQMwj9bkx_KQufXTfAD6IbQ-YHKZzatiL8VH9VbZhI2dx47YDTBE8mwI_wDRyjZM0QRQlDGGWYkRh-gKcErdmGbjZphhjBLD3F8Dt1RWtCMaM0IxmhT5hgRGNQsnXGvszrMW3J6Q9Ifp7J?type=png" alt="Coroutine Diagram"></a></div></div></td></tr></tbody></table>
<p>Even though this workflow will execute to completion effectively once, it will still be partially executed four different times. The results of activities are cached so that only failed activities will be called again. Successful activities get skipped.</p>
<p>But notice that any code we write between these calls will be called multiple times. That’s why your code needs to be <strong>deterministic</strong> inside of workflow methods! If your code has four executions, each at different times, they must still all behave the same. There are no such limitations within activity methods.</p>
<h1>Step By Step</h1>
<p>The first time the workflow executes, it will reach the call to <code>SendEmailVerificationEmailActivity</code> , start that activity, and then exit. Workflows suspend execution while an activity is running. After the <code>SendEmailVerificationEmailActivity</code> finishes, it will resume execution of the workflow. This brings us to…</p>
<p>The second time the workflow is executed, it will reach the call to <code>SendEmailVerificationEmailActivity</code> and skip it because it will already have the result of that activity. Then it will reach the call to <code>await()</code> which allows the workflow to wait for an external signal. In this case, it will come from the user clicking on the verification link they receive in their email. Once the workflow is signaled then it will execute for…</p>
<p>The third time, both the calls to <code>SendEmailVerificationEmailActivity</code> and <code>await()</code> are skipped. This means that the <code>VerifyEmailActivity</code> will be started. After the final activity has executed we still have…</p>
<p>The final time the workflow is called, there is nothing left to do so the workflow completes.</p>
<p>Now let’s take a look at the activities.</p>
<p>The first activity just sends the user an email.</p>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">namespace App\Workflows\VerifyEmail;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use App\Mail\VerifyEmail;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Illuminate\Support\Facades\Mail;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Workflow\Activity;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">class SendEmailVerificationEmailActivity extends Activity</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function execute($email)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        Mail::to($email)-&gt;send(new VerifyEmail($this-&gt;workflowId()));</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<p>The email contains a temporary signed URL that includes the workflow ID.</p>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">namespace App\Mail;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Illuminate\Bus\Queueable;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Illuminate\Mail\Mailable;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Illuminate\Mail\Mailables\Content;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Illuminate\Mail\Mailables\Envelope;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Illuminate\Queue\SerializesModels;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Illuminate\Support\Facades\URL;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">class VerifyEmail extends Mailable</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    use Queueable, SerializesModels;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    private $workflowId;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function __construct($workflowId)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $this-&gt;workflowId = $workflowId;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function envelope()</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        return new Envelope(</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            subject: 'Verify Email',</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        );</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function content()</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        return new Content(</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            view: 'emails.verify-email',</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            with: [</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                'url' =&gt; URL::temporarySignedRoute(</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                    'verify-email',</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                    now()-&gt;addMinutes(30),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                    ['workflow_id' =&gt; $this-&gt;workflowId],</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                ),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            ],</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        );</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function attachments()</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        return [];</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<p>The user gets the URL in a clickable link.</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">&lt;a href="{{ $url }}"&gt;verification link&lt;/a&gt;</span><br></div></code></pre></div></div>
<p>This link takes the user to the <code>verify-email</code> route from our API routes, which will then start the final activity.</p>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">namespace App\Workflows\VerifyEmail;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use App\Models\User;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">use Workflow\Activity;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">class VerifyEmailActivity extends Activity</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">{</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    public function execute($email, $password)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $user = new User();</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $user-&gt;name = '';</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $user-&gt;email = $email;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $user-&gt;email_verified_at = now();</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $user-&gt;password = $password;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        $user-&gt;save();</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<p>We have created the user and verified their email address at the same time. Neat!</p>
<h1>Wrapping Up</h1>
<p>If we take a look at the output of <code>php artisan queue:work</code> we can better see how the workflow and individual activities are interleaved.</p>
<p><img decoding="async" loading="lazy" src="https://miro.medium.com/max/1400/1*q6-r41SN-uWfzp6p7Z4r8g.webp" alt="queue worker" class="img_ev3q"></p>
<p>We can see the four different executions of the workflow, the individual activities and the signal we sent.</p>
<p>The <a href="https://github.com/durable-workflow/workflow" target="_blank" rel="noopener noreferrer" class="">Workflow</a> library is heavily inspired by <a href="https://temporal.io/" target="_blank" rel="noopener noreferrer" class="">Temporal</a> but powered by <a href="https://laravel.com/docs/9.x/queues" target="_blank" rel="noopener noreferrer" class="">Laravel Queues</a>.</p>
<p>Thanks for reading!</p>]]></content>
        <author>
            <name>Richard</name>
            <uri>https://github.com/rmcdaniel</uri>
        </author>
        <category label="emails" term="emails"/>
        <category label="verification" term="verification"/>
        <category label="signed-urls" term="signed-urls"/>
    </entry>
</feed>