Minimal graph workflow library for PHP — a port of The-Pocket/PocketFlow.
- Nodes with
prep→exec→post - Flows that route on action strings (
default,continue, …) - Shared state bag mutated across hops (Flows clone nodes; put durable data in
$shared) - Async via ReactPHP Promise (
AsyncNode/AsyncFlow) - Batch helpers for list and parallel fan-out
Package: projectsaturnstudios/pocketflow-php
Namespace: ProjectSaturnStudios\PocketFlow\
- PHP 8.4+
react/promise(required for async APIs; syncFlow/Nodedo not need an event loop)
composer require projectsaturnstudios/pocketflow-php<?php
use ProjectSaturnStudios\PocketFlow\Flow;
use ProjectSaturnStudios\PocketFlow\Node;
class GreetNode extends Node
{
public function prep(mixed &$shared): mixed
{
return $shared['name'] ?? 'World';
}
public function exec(mixed $prep_res): mixed
{
return "Hello, {$prep_res}!";
}
public function post(mixed &$shared, mixed $prep_res, mixed $exec_res): mixed
{
$shared['greeting'] = $exec_res;
return 'default'; // next action
}
}
class PrintNode extends Node
{
public function post(mixed &$shared, mixed $prep_res, mixed $exec_res): mixed
{
echo $shared['greeting'], PHP_EOL;
return null; // end flow
}
}
$greet = new GreetNode;
$print = new PrintNode;
$greet->next($print);
$shared = ['name' => 'PocketFlow'];
(new Flow($greet))->run($shared);$nodeA->on('ok')->next($nodeB);
$nodeA->next($nodeC, 'retry');$tick->next($tick, 'continue');
// return 'continue' to loop, or null to stopuse ProjectSaturnStudios\PocketFlow\AsyncFlow;
use ProjectSaturnStudios\PocketFlow\AsyncNode;
use React\Promise\PromiseInterface;
use function React\Promise\resolve;
class FetchNode extends AsyncNode
{
public function exec_async(mixed $prep_res): PromiseInterface
{
return resolve(['ok' => true]);
}
public function post_async(mixed $shared, mixed $prep_res, mixed $exec_res): PromiseInterface
{
$shared['result'] = $exec_res;
return resolve('default');
}
}
$shared = [];
$promise = (new AsyncFlow(new FetchNode))->run_async($shared);
// $promise is a React\Promise\PromiseInterfaceArray $shared values are wrapped in a SharedStore for the async run so mutations persist across promise hops, then copied back into your array when the promise settles.
Nested AsyncFlow graphs work: AsyncFlow extends AsyncNode, matching the Python MRO intent.
BatchNode/BatchFlow— sequential items / param setsAsyncBatchNode/AsyncParallelBatchNode— sequential vsPromise\allAsyncBatchFlow/AsyncParallelBatchFlow— batch orchestration
| Method | Role |
|---|---|
Node::run(&$shared) |
Run a single node (warns if successors exist — use Flow) |
Flow::run(&$shared) |
Orchestrate from the start node |
AsyncNode::run_async(&$shared) / AsyncFlow::run_async(&$shared) |
Promise-based entry |
post / post_async return value |
Action key for the next successor (default if null/non-string) |
Retries: Node / AsyncNode honor max_retries and wait (seconds) via _exec.
composer install
composer test
# or: vendor/bin/pestCI runs Pest on PHP 8.4.
| Feature | Python | This package |
|---|---|---|
| Graph + action routing | Yes | Yes |
| Shared mutable store | dict | array / SharedStore |
| Async | asyncio | ReactPHP Promise |
| Batch / parallel batch | Yes | Yes |
MIT — same spirit as the original PocketFlow.
- The-Pocket/PocketFlow — original design
- ReactPHP — promises for async nodes