Create Agentic Apps with NodeGraph and Laravel
A compact state-graph runtime for Laravel — nodes, edges, threads, and checkpoints, all stored in your database.
In my last article, I shared an idea about building a Laravel app where users interact primarily with AI agents. I kept exploring it, and eventually had the urge to package the pieces I kept rewriting. That's how NodeGraph was born.
NodeGraph is a compact state-graph runtime for Laravel. It makes it easy to define controlled flows where each node is either an AI agent or custom code that runs when the flow reaches a particular state — much like LangGraph.
You might ask: why not just use LangGraph? LangGraph is built for Python and JavaScript, and I really like working with PHP and Laravel. Beyond preference, the key difference is that NodeGraph is fully integrated with Laravel. State and checkpoints live in the database, which gives you flexibility in when and how flows run, while keeping a full record of everything that happened. That's useful when you're building complex state machines and need to trace what each run did.
I.Define your states
The first thing to create is a State — a PHP enum that holds every possible state a flow can be in:
use Taecontrol\NodeGraph\Contracts\HasNode;
enum OrderState: string implements HasNode
{
case Start = 'start';
case Charge = 'charge';
case Done = 'done';
public function node(): string
{
return match ($this) {
self::Start => \App\Nodes\StartNode::class,
self::Charge => \App\Nodes\ChargeNode::class,
self::Done => \App\Nodes\DoneNode::class,
};
}
}The node() method maps each state to the Node class that runs
when the flow is in that state.
II.Write the nodes
A Node is where the work happens. Each one receives a shared context and returns a Decision describing what should happen next:
namespace App\Nodes;
use App\Decisions\SimpleDecision;
use App\Enums\OrderState;
use App\Events\OrderEvent;
use Taecontrol\NodeGraph\Node;
class StartNode extends Node
{
public function handle($context): SimpleDecision
{
$d = new SimpleDecision(OrderState::Charge);
$d->addMetadata('from', 'start');
$d->addEvent(new OrderEvent('start'));
return $d;
}
}
class ChargeNode extends Node
{
public function handle($context): SimpleDecision
{
// ... charge logic ...
$d = new SimpleDecision(OrderState::Done);
$d->addMetadata('from', 'charge');
$d->addEvent(new OrderEvent('charged'));
return $d;
}
}
class DoneNode extends Node
{
public function handle($context): SimpleDecision
{
$d = new SimpleDecision(null); // terminal
$d->addMetadata('from', 'done');
$d->addEvent(new OrderEvent('done'));
return $d;
}
}The Context is a small object your nodes share. You can stuff it with whatever your flow needs, but it's required to carry a Thread:
use Taecontrol\NodeGraph\Context;
use Taecontrol\NodeGraph\Models\Thread;
class OrderContext extends Context
{
public function __construct(protected Thread $thread) {}
public function thread(): Thread
{
return $this->thread;
}
}And the Decision is the contract between a node and the runtime:
namespace App\Decisions;
use Taecontrol\NodeGraph\Decision;
class SimpleDecision extends Decision {}Inside a node you can add events, set a new state, or write metadata onto the Decision. When the runtime processes it, the Thread's current state is updated to whatever you specified, the events are dispatched, metadata is persisted on the Thread, and a new checkpoint is created.
III.Wire it into a graph
The Graph is where nodes become a flow. You declare the edges and an initial state:
use Taecontrol\NodeGraph\Graph;
use App\Enums\OrderState;
class OrderGraph extends Graph
{
public function define(): void
{
$this->addEdge(OrderState::Start, OrderState::Charge);
$this->addEdge(OrderState::Charge, OrderState::Done);
// Done has no outgoing edges, so it's terminal.
}
public function initialState(): OrderState
{
return OrderState::Start;
}
}IV.Running the flow
To execute, create a Thread, wrap it in a Context, and ask the graph to run:
use Taecontrol\NodeGraph\Models\Thread;
$thread = Thread::create([
'threadable_type' => \App\Models\Order::class,
'threadable_id' => (string) \Illuminate\Support\Str::ulid(),
'metadata' => [],
]);
$context = new \App\Contexts\OrderContext($thread);
$graph = app(\App\Graphs\OrderGraph::class);
$graph->run($context); // Start -> Charge
$graph->run($context); // Charge -> Done
$graph->run($context); // Done is terminal; finished_at is setThe Thread uses a polymorphic relation, so you can hang it off any
model in your app. Each call to run() advances the flow by one
step, dispatches events, and writes a checkpoint — so you always
know exactly where a particular Thread is and how it got there.
V.Where this is going
That's the whole API. The package is still in beta, but it already feels like a nice fit for agentic workflows: each AI agent becomes a Node, the database remembers every step, and the rest of your Laravel app can observe the flow through events and the Thread's metadata. More details and install instructions are in the NodeGraph repository — let me know what you think.