PHPackages                             milpa/orchestrator - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Framework](/categories/framework)
4. /
5. milpa/orchestrator

ActiveLibrary[Framework](/categories/framework)

milpa/orchestrator
==================

Generic event-sourced process engine for the Milpa PHP framework: process definitions composed from milpa/workflow states/transitions/gates, a pure reducer folding an EventStoreInterface log into process state, an auto-advancing runner, and human-gate decision surfaces via milpa/live.

v0.5.2(1w ago)0751Apache-2.0PHPPHP &gt;=8.3CI passing

Since Jul 9Pushed 1w agoCompare

[ Source](https://github.com/getmilpa/orchestrator)[ Packagist](https://packagist.org/packages/milpa/orchestrator)[ RSS](/packages/milpa-orchestrator/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (10)Dependencies (44)Versions (14)Used By (1)

 [   ![Milpa](https://raw.githubusercontent.com/getmilpa/core/main/art/lockup/milpa-lockup-v-color-light.svg)  ](https://github.com/getmilpa)

Milpa Orchestrator
==================

[](#milpa-orchestrator)

> **Event-sourced process orchestration** for the Milpa PHP framework: **everything is a process**, a process is a state machine, and **state is a projection of an append-only log**. Human gates carry live decision surfaces whose options map **1:1** to the process's own transitions; self-approval is refused by construction; three MCP tools drive it all. The greenhouse (`example-agent-ready-blog`) proved the loop before this package froze the contracts.

[![CI](https://github.com/getmilpa/orchestrator/actions/workflows/ci.yml/badge.svg)](https://github.com/getmilpa/orchestrator/actions/workflows/ci.yml)[![Packagist](https://camo.githubusercontent.com/c700ac46e6ac34ae8978d96b27c36c97ef57ac08fec65e3f9bd56bbabf2226a2/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6d696c70612f6f7263686573747261746f722e737667)](https://packagist.org/packages/milpa/orchestrator)[![PHP](https://camo.githubusercontent.com/ca03f11ea27dac4dedc8ad56a7bdfc4a9ff5feb825055f9d2983616115076607/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7068702d254532253839254135253230382e332d3737376262342e737667)](https://www.php.net/)[![License](https://camo.githubusercontent.com/798509b4df525f56802b56f8096862487f08023e3d7561c68656f8dab10d0d6e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4170616368652d2d322e302d626c75652e737667)](LICENSE)[![Docs](https://camo.githubusercontent.com/c6dc6a3411e15b0ac7cc4583e8e6a8144181caedb82f5d98753353decda06d77/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f646f63732d4150492532307265666572656e63652d626c75652e737667)](https://getmilpa.github.io/orchestrator/)

`milpa/orchestrator` is the process engine of the Milpa family. It takes a `milpa/workflow`state machine — states, transitions, and human gates — and runs it as an **event-sourced process**: nothing stores `current_state`, every read replays an append-only log through a pure reducer, and human decision points surface as `milpa/live` components whose options can never drift from the transitions they actually resolve. It has **no ORM, no HTTP kernel, no product coupling** — the domain (a blog post, an invoice, a support ticket) lives entirely in the consumer's decision-surface factory and its `process.terminal` listener.

Install
-------

[](#install)

```
composer require milpa/orchestrator
```

The thesis
----------

[](#the-thesis)

Two ideas hold the whole engine together:

1. **Everything is a process, and a process is a state machine.** A `ProcessDefinition` is a set of `milpa/workflow` `StateDefinition`s wired by `TransitionDefinition`s, exactly one marked initial. A state whose outgoing transitions carry a `GateDefinition` is a **human decision point**; every other state advances automatically.
2. **State is a projection of the log, never a stored field.** Starting or advancing a process only ever *appends events* to an `EventStoreInterface`. The current state is whatever the pure `Reducer` folds those events into — recomputed fresh on every read. Two handles built from the same instance id over the same log always agree, and neither can cache a stale answer.

Everything else — the auto-advancing runner, the human gate, the three MCP tools — is built on those two invariants.

Quick example
-------------

[](#quick-example)

Define a three-state publishing process — `draft → review → published`, with a human gate on `review` and a `reject` transition that loops back to `draft` for revision:

```
use Milpa\Orchestrator\ProcessDefinition;
use Milpa\Workflow\Entities\GateDefinition;
use Milpa\Workflow\Entities\StateDefinition;
use Milpa\Workflow\Entities\TransitionDefinition;
use Milpa\Workflow\Enums\ApprovalPolicy;

$draft     = (new StateDefinition())->setDomain('publish_post')->setCode('draft')->setLabel('Draft')->setIsInitial(true);
$review    = (new StateDefinition())->setDomain('publish_post')->setCode('review')->setLabel('In review');
$published = (new StateDefinition())->setDomain('publish_post')->setCode('published')->setLabel('Published')->setIsTerminal(true);

$gate = (new GateDefinition())
    ->setDomain('publish_post')->setCode('review_gate')->setName('Editorial review')
    ->setRequesterRole('author')->setApproverRole('editor')
    ->setApprovalPolicy(ApprovalPolicy::SINGLE);

$submit  = (new TransitionDefinition())->setDomain('publish_post')->setCode('submit')->setFromState($draft)->setToState($review);
$approve = (new TransitionDefinition())->setDomain('publish_post')->setCode('approve')->setFromState($review)->setToState($published);
$reject  = (new TransitionDefinition())->setDomain('publish_post')->setCode('reject')->setFromState($review)->setToState($draft);
$approve->addGateDefinition($gate);   // both outcomes share the SAME gate: one checkpoint,
$reject->addGateDefinition($gate);    // two options (approve | reject)

$definition = new ProcessDefinition([$draft, $review, $published], [$submit, $approve, $reject]);
```

Wire the engine and expose it through the three tools:

```
use Milpa\EventStore\FileEventStore;
use Milpa\Eventing\EventDispatcher;
use Milpa\Orchestrator\HumanGate;
use Milpa\Orchestrator\ProcessDefinitionRegistry;
use Milpa\Orchestrator\ProcessRunner;
use Milpa\Orchestrator\Tools\ProcessInstantiateTool;
use Milpa\Orchestrator\Tools\ProcessListPendingApprovalsTool;
use Milpa\Orchestrator\Tools\ProcessSubmitDecisionTool;
use Milpa\ToolRuntime\Contracts\ToolContext;
use Psr\Log\NullLogger;

$store      = new FileEventStore('/tmp/posts.jsonl');   // the append-only log
$dispatcher = new EventDispatcher(new NullLogger());    // milpa/events
$dispatcher->subscribe('process.terminal', function (string $name, array $payload): void {
    // Reaching `published` runs the real domain effect HERE — the engine itself touches no
    // domain entity. $payload = {instance_id, final_state, context}.
});

$registry = new ProcessDefinitionRegistry();
$registry->register('publish_post', $definition);

$gate   = new HumanGate(new YourDecisionSurfaceFactory());   // a milpa/live surface, consumer-supplied
$runner = new ProcessRunner($dispatcher);

$instantiate = new ProcessInstantiateTool($store, $gate, $runner, $registry);
$instantiate->setCurrentContext(ToolContext::mcp('req-1', 'agent:author', ['*']));
$list   = new ProcessListPendingApprovalsTool($store, $gate, $registry);
$submit = new ProcessSubmitDecisionTool($store, $gate, $runner, $registry);
```

Now drive the loop — instantiate, hit the gate, submit a decision, reach terminal, and prove the state was never stored by replaying it from a fresh log:

```
use Milpa\Orchestrator\ProcessInstance;

// 1. Instantiate — auto-advances draft --submit--> review and PARKS at the human gate.
$started    = $instantiate->instantiate('publish_post', ['post_id' => 42]);
$instanceId = $started->data['instance_id'];
$started->data['current_state'];   // 'review' — the runner stopped at the gate, not past it

// 2. The gate is pending; its options are projected 1:1 from the process's OWN transitions.
$pending = $list->list()->data['pending'][0];
$pending['assignee'];   // 'editor'
$pending['options'];    // ['approve', 'reject']
$gateId  = $pending['gate_id'];

// 3. An editor — NOT the author — resolves it. Self-approval is refused by construction:
//    submitting as 'agent:author' here returns error SELF_APPROVAL_FORBIDDEN instead.
$done = $submit->submit($instanceId, $gateId, 'approve', 'human:editor');
$done->data['current_state'];   // 'published' — auto-advanced past the gate to terminal;
                                //  `process.terminal` fired exactly once.

// 4. State is a projection: a FRESH store + handle over the SAME log reconstructs it, no cache.
$replayed = new ProcessInstance($instanceId, $definition);
$replayed->currentState(new FileEventStore('/tmp/posts.jsonl'));   // 'published'
```

Had the editor chosen `reject`, the runner would have driven `review --reject--> draft --submit--> review` and re-opened a fresh gate — the revise-and-resubmit loop, all inside that one `process_submit_decision` call. (This exact loop is exercised end to end in `tests/ProcessLoopTest.php`.)

Composes the family
-------------------

[](#composes-the-family)

The orchestrator writes almost no primitives of its own — it *composes* the packages below the process tier and adds only the folding, running, and gating that turn them into a process engine:

PackageRole in a process[`milpa/event-store`](https://packagist.org/packages/milpa/event-store)**The log.** Every start, transition, gate opening, and decision is an `Event` appended to an `EventStoreInterface`. The engine stores nothing else.[`milpa/workflow`](https://packagist.org/packages/milpa/workflow)**The gates + self-approval rule.** `ProcessDefinition` is built from workflow `StateDefinition`/`TransitionDefinition`/`GateDefinition`; `HumanGate` delegates the D9 anti-self-approval check to workflow's `GateServiceInterface` rather than reimplementing it.[`milpa/live`](https://packagist.org/packages/milpa/live)**The decision surfaces.** A `DecisionSurfaceInterface` is a `milpa/live` component whose `options()` must equal the gate's transitions 1:1 — `PendingDecision`'s constructor enforces that invariant, so a stale artifact fails loudly instead of offering actions the gate does not have.[`milpa/tool-runtime`](https://packagist.org/packages/milpa/tool-runtime)**The three MCP tools.** `process_instantiate`, `process_list_pending_approvals`, and `process_submit_decision` are `#[Tool]`-attributed methods that run through the tool-runtime pipeline like any other agent-callable tool.[`milpa/events`](https://packagist.org/packages/milpa/events)**The reducer/terminal seam.** The reference `MilpaEventDispatcherInterface` implementation; `ProcessRunner` dispatches `process.terminal` through it exactly once per instance, where a consumer runs whatever domain effect reaching a terminal state should trigger.Because the engine is domain-agnostic, the two things it does **not** own are exactly the two a consumer supplies: a `DecisionSurfaceFactoryInterface` (what a gate's surface renders for its domain) and a `process.terminal` listener (what reaching a terminal state *does*).

Proven in a greenhouse
----------------------

[](#proven-in-a-greenhouse)

Before these contracts were frozen, the whole loop was grown and validated inside **`example-agent-ready-blog`** — the family's greenhouse — as a real `PublishPostProcess`: an agent drafts a post, submits it, a human editor approves or rejects it through a live decision surface, and reaching `published` actually publishes the post via a `process.terminal` listener. This package is that proven loop, lifted out domain-free: the greenhouse kept the blog; the orchestrator kept the engine.

Requirements
------------

[](#requirements)

- PHP **≥ 8.3**
- [`milpa/core`](https://packagist.org/packages/milpa/core) **^0.6**
- [`milpa/event-store`](https://packagist.org/packages/milpa/event-store) **^0.1**
- [`milpa/workflow`](https://packagist.org/packages/milpa/workflow) **^0.1.2**
- [`milpa/events`](https://packagist.org/packages/milpa/events) **^0.2**
- [`milpa/live`](https://packagist.org/packages/milpa/live) **^0.1**
- [`milpa/tool-runtime`](https://packagist.org/packages/milpa/tool-runtime) **^0.5.1**

Documentation
-------------

[](#documentation)

**Full API reference: [getmilpa.github.io/orchestrator](https://getmilpa.github.io/orchestrator/)** — generated straight from the source DocBlocks and dressed with the Milpa design system.

Contributing
------------

[](#contributing)

Contributions are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md). Please report security issues via [SECURITY.md](SECURITY.md), and note that this project follows a [Code of Conduct](CODE_OF_CONDUCT.md).

License
-------

[](#license)

[Apache-2.0](LICENSE) © Rodrigo Vicente - TeamX Agency.

---

Milpa is designed, built, and maintained by **[Rodrigo Vicente - TeamX Agency](https://teamx.agency/?utm_source=github&utm_medium=readme&utm_campaign=milpa&utm_content=orchestrator)**.

###  Health Score

44

—

FairBetter than 90% of packages

Maintenance98

Actively maintained with recent releases

Popularity12

Limited adoption so far

Community12

Small or concentrated contributor base

Maturity46

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 76.5% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~3 days

Total

12

Last Release

11d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/1993784?v=4)[rodrigomx](/maintainers/rodrigomx)[@rodrigomx](https://github.com/rodrigomx)

---

Top Contributors

[![rodrigoteamx](https://avatars.githubusercontent.com/u/269849276?v=4)](https://github.com/rodrigoteamx "rodrigoteamx (26 commits)")[![github-actions[bot]](https://avatars.githubusercontent.com/in/15368?v=4)](https://github.com/github-actions[bot] "github-actions[bot] (8 commits)")

---

Tags

agenticevent-sourcedevent-sourcingframeworkmilpaorchestrationphpprocess-engineprocess-orchestrationsagasphpframeworkevent sourcingorchestrationsagasprocess-enginemilpa

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/milpa-orchestrator/health.svg)

```
[![Health](https://phpackages.com/badges/milpa-orchestrator/health.svg)](https://phpackages.com/packages/milpa-orchestrator)
```

###  Alternatives

[utopia-php/orchestration

Lite &amp; fast micro PHP abstraction library for container orchestration

18240.6k7](/packages/utopia-php-orchestration)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
