How typed events, an idempotent signal bus and a bounded watchdog let several PHP agents cooperate without putting scheduling or transport inside the framework.
I run a small holding company, and like many people I wanted a "Jarvis": a set of agents that prepare my morning briefing, watch operations, and tell me when something stops working. Instead of one giant agent with a giant prompt, I tried a mesh: small, role-specific agents coordinated by a Neuron AI workflow.
The result is a compact example, proposed as PR #683 (examples/agent/jarvis-mesh.php and docs/jarvis-mesh.md). This post explains the design choices.
Transparency note: this prototype was built with AI coding assistance, and it is meant as a starting point, not production code. The PR is still open and has not been reviewed or merged.
The idea: let the workflow own the sequence, not the agents
Three agents each have one job:
- Research prepares sourced highlights for the day.
- Operations summarizes status and risks, in light of the research.
- Briefing composes the final daily briefing from both.
They never call each other. A Neuron Workflow routes between them using typed events: BriefingStarted -> ResearchCompleted -> OperationsCompleted -> BriefingComposed. Each node takes one event type and returns the next one, so the graph of the business process is readable straight from the type signatures.
Adapters instead of framework code
The example deliberately defines four small interfaces that your application implements:
-
JarvisSignalBus: publish a typed signal, idempotent by ID. -
JarvisKnowledgeStore: shared notes between agents, idempotent by key. -
JarvisHeartbeatStore: when was each agent last seen? -
JarvisAgentFactory: returns a freshly configuredAgentInterfaceper role and thread.
They are passed to the workflow as a WorkflowResources object, rebuilt for each execution segment. Provider, tools, instructions and message store all live in your factory, so the workflow code never changes when you swap an LLM or a storage backend.
Surviving replays
Workflows can be interrupted and resumed, so every side effect has to tolerate being run twice. Two mechanisms help:
- Agent calls go through the node's
memoize(), so a completed agent call is not repeated when an interrupted run resumes. - Signals and knowledge writes use stable keys (for example
"{workflowId}/research.completed"), so a repeated publication does not create duplicates.
One honest limit, also stated in the docs: memoization cannot make an external side effect exactly-once. Idempotent adapters are your responsibility.
A bounded watchdog for stale agents
The second workflow, WatchdogWorkflow, is a single-pass sweep. Your scheduler (cron, a queue, whatever you already use) starts a fresh execution per sweep with a list of agents, a timestamp and a staleness threshold. For each agent, one node checks the last heartbeat and, if it is stale or missing, writes a note and publishes an agent.heartbeat.stale signal under a key derived from the agent and its last-seen time. Running the same sweep twice therefore raises the same alert once.
The workflow is capped at one step and a 60-second lease, so it cannot loop forever. Scheduling, heartbeat production, alert delivery and monitoring policy stay in the application.
Why not use workflow signals as the bus?
Neuron's Workflow::signal() answers the current matching wait. It is not a broadcast and not a queue. For fan-out between independent agents you need a real shared bus, which is why the example treats it as an application integration boundary. The rule I settled on: transport and scheduling outside workflow nodes; business sequencing and state inside the graph.
Wiring it up
In your application, you provide the adapters and a persistence backend, then run:
$workflow = (new DailyBriefingWorkflow($date))
->setPersistence($workflowPersistence)
->setResources($resourcesFactory);
$state = $workflow->run();
$briefing = $state->get('briefing');
Use durable, shared implementations for the bus and the knowledge store, and a multi-process-safe persistence backend if workers may overlap.
How I tested it
I ran the example against Neuron 4.1 with simulated agents and in-memory adapters (no real LLM, no real database). The briefing ran end to end; an agent that failed mid-run was resumed without repeating the completed research step; and the watchdog raised one alert for a stale agent and one for a missing agent, ignored a healthy one, and did not duplicate alerts on a second sweep. I have not run the full Neuron test suite or PHPStan, and re-running an already completed workflow with the same ID calls the agents again (signals stay deduplicated).
What I would build next
- A real heartbeat producer inside each agent worker.
- Alert delivery (chat, email) as a consumer of
agent.heartbeat.stale. - Proper unit tests for the adapters with in-memory fakes.
Thanks to the Neuron maintainers for the framework and for the invitation to share this. Feedback on the design is very welcome on the PR.
Jules Langlois
Top comments (0)