The shift

Traditional AI stops at a response. Actuate keeps going until quality is measured, not assumed.

Typical stack
prompt
model
response
done — hope it's right

Generate once. There is no measured signal telling you whether the output actually met the bar — only the model finishing talking.

Actuate
setpoint
plant
sensor
error
controller
actuator
↺ plant

Iterate until a measurable setpoint is met, or until stability guards call it: converged, exhausted, oscillating, or timed out.

Why Actuate

If you want stability, gain, and convergence around model output — not just a graph of tools.

A DAG of tools calls for a workflow engine. A conversation calls for a chat product. Actuate is for teams that need oscillation detection and a measured path to “good enough,” every run.

Typical stackActuate
Generate once and hopeIterate until a measurable setpoint is met
Prompt is the productThe control system is the product
Hidden retry wrappersImmutable signals and append-only events
Graph = the applicationGraph = topology inside a versioned specification
Chat UIIndustrial control console
The idea

Classical control theory maps onto Actuate one-to-one.

Signals are values on the wire and are never mutated. Events are facts in the log and are never rewritten. Iterations, status, and convergence progress are projections of the event stream — not a second mutable database.

Control theoryActuate
PlantGenerator — LLM, NVIDIA NIM, custom OpenAI-compatible endpoint, or stub
SensorEvaluator — RuleEvaluator, LLMJudgeEvaluator, SimilarityEvaluator
ErrorErrorSignal — setpoint minus measured
ControllerTopology-blind Controller — rule-based or PID
ActuatorCorrector — PromptCorrector, strategy / context / output
SetpointSetPoint.target
Stability / saturationStabilityGuard — max iterations, timeout
OscillationConvergencePolicy
Feedback pathPlant output structurally reaches an actuator, policy on by default
runtime vs history — an ExecutionSession is the live in-memory cursor; a Run is the persisted historical record.
Architecture

Frozen constraints. A clear boundary between what decides and what executes.

A graph is how topology is represented — NetworkX plus port-typed edges — and how the console runs multi-agent labs. It is not the aggregate root.

Presentation
React console · FastAPI · WebSocket
ControlSystem
Specification (immutable, versioned) → Topology
ExecutionEngine
walks topology · fans events to sinks · Controller + Scheduler
signals on ports
Plant
Sensor
Merge
Actuator
↺ feedback
EventLog → PostgresOpenTelemetrylive UI
Domain hierarchy
Workspace
what the user is building in
ControlSystem
what the user is building
Specification
immutable snapshot: topology + policy + bindings
Run
one execution, event-sourced
Iteration
projection: one pass around the feedback path
Layer responsibilities
LayerResponsibilityNot responsible for
actuate.domainTypes — signals, events, topology, policy, registryI/O
actuate.engineWalk topology, invoke capabilities, append eventsControl law, SQL, HTTP
ControllerError + objective + history → control decisionGraph traversal
SchedulerSequential / future parallel dispatchWhat to run
RunStoreDurable workspace / system / spec / event logBinary blobs
EventSinkTrace, persist, WebSocket, MLflowOrchestration
CapabilityRegistryDiscover and instantiate pluginsExecution
Two ways in

A Python package to embed, and an operator console to run.

actuate — python package

Install with the extras you need. Import ExecutionEngine, GraphRunner, plants, sensors, and stores directly — this is how you embed Actuate inside another service or script.

pip
pip install -e ".[plants,persistence,ui]"
control console

FastAPI + React. The operator UI for the same package, run with python -m actuate.api and ui/frontend. Same capabilities, optional extras.

entry points
actuate            # API server
actuate-bootstrap  # seed Postgres
Library example — closed loop
python
import asyncio
from actuate.domain.policy import LoopPolicy, SetPoint, StabilityGuard
from actuate.domain.specification import create_specification
from actuate.domain.templates import standard_closed_loop
from actuate.engine import ExecutionEngine
from actuate.plugins import register_builtins

async def main() -> None:
    spec = create_specification(
        control_system_id="demo",
        version_number=1,
        topology=standard_closed_loop(
            plant_name="stub",
            sensor_name="rule",
            sensor_params={"required_phrases": ["MUST-INCLUDE"]},
        ),
        policies=LoopPolicy(
            set_point=SetPoint(target=0.95),
            stability=StabilityGuard(max_iterations=6),
        ),
    )
    run = await ExecutionEngine().run(
        spec,
        registry=register_builtins(),
        initial_prompt="Write a short answer.",
    )
    print(run.id, run)

asyncio.run(main())

you do not run chat-completions yourself — pass model, api_key, and an optional api_base into the plant. Actuate calls the provider.

Quick start

Four steps from clone to a running control console.

Console API auth is on by default. Live LLM calls need a saved provider key under Settings.

01

Clone

Grab the source. Swap the org for your fork if you haven't published a canonical repo yet.

shell
git clone https://github.com/actuate-ai/actuate.git
cd actuate
02

Postgres

Actuate's production RunStore is Postgres, and only Postgres. Bring it up with Docker Compose.

shell
docker compose up -d postgres

# unix
export DATABASE_URL=postgresql+psycopg://actuate:actuate@localhost:5432/actuate
03

Install & boot the API

Startup bootstraps Postgres idempotently — a default workspace, four control systems, specs, and provider base URLs. Existing API keys are never overwritten.

shell
pip install -e ".[ui,persistence,plants,dsl,dev]"
python -m actuate.api
04

Open the console

Start the React frontend and open localhost:5173. Ctrl/Cmd + K opens the command palette.

shell
cd ui/frontend
npm install
npm run dev
re-run the seed anytime with python -m actuate.persistence.bootstrap or actuate-bootstrap.
Control console

Twelve screens for treating named runs as operations, not chat history.

Dashboard

Dense KPIs, charts, status/provider mix, template sizes, searchable activity

New Run

Multi-agent graph (default) or control loop; named runs

Live Session

Loop I/O; selected iteration opens as the detail panel

Graph run

Tools + I/O logs; rerun a node as a revision; export JSON pack

Runs

Named loop + graph history with in-place search

Loop Designer

Drag-drop specialists, labs, cursor zoom, pan, minimap

Benchmarks

Cohort stats plus compare two named runs

Memory

Trajectories in Postgres when DATABASE_URL is set

Models

Provider catalog + full agent system prompts

Plugins

Capability registry

Observability

Score, tokens, latency, status mix from activity

Settings

Persist keys + custom bases to Postgres

Actuate vs LangGraph

LangGraph is a workflow runtime. Actuate is a control system with an optional graph inside it.

Use LangGraph for LangChain's graph SDK. Use Actuate for measure → correct → converge around generation, with a console that treats named runs as operations.

LangGraphActuate
Product identityThe graph / state machineControlSystem → Specification → event-sourced Run
LLM roleA node among nodesA plant — and, on graphs, a specialist that may call tools
StoppingGraph reaches an end nodeConvergence, exhaustion, oscillation, timeout, or token budget
QualityWhatever you codeMeasured sensor / judge scores vs a setpoint
MemoryCheckpoint / thread stateRetrieval of successful trajectories, Postgres-backed
ParallelismFan-out if you model itReady nodes with no unfinished parents run together
AuditTraces if you add themAppend-only events + exportable run pack
Models & plants

Bring any provider.

ProviderHow
StubTests only (allow_stub). Not a console default
OpenAI, Anthropic, Gemini, Groq, OpenRouterLiteLLM + env / Settings keys
NVIDIA NIMhttps://integrate.api.nvidia.com/v1 + NVIDIA_API_KEY
OllamaLocal http://localhost:11434
CustomYour URL + key + model, OpenAI chat-completions compatible
Agent tools

What a graph specialist can reach for.

ToolWhat it does
web_searchDuckDuckGo instant-answer search
http_getGET a public https URL — private / loopback / metadata blocked, ~80KB cap
recall_memorySimilar past converged trajectories
calculatorArithmetic
utc_nowUTC timestamp
list_connectionsThis node's parents/children
handoffStructured packet for downstream nodes
Repository layout

One tree, clearly separated concerns.

tree
actuate/
  domain/          ControlSystem, Specification, Topology, Signals, Events, ExecutionSession
  engine/          ExecutionEngine, controllers, fusion, scheduler, NetworkX helper
  plants/          StubGenerator, LiteLLMAdapter (OpenAI, Anthropic, Gemini, Groq,
                    OpenRouter, NVIDIA NIM, Ollama, custom OpenAI-compatible)
  sensors/         RuleEvaluator, LLMJudgeEvaluator, SimilarityEvaluator
  actuators/       PromptCorrector, OutputCorrector, ContextCorrector, StrategyCorrector
  retry/           Exponential, diversity, temperature sweep, model switch, perturbation
  memory/          Vector store + cosine retriever + learning graph
  persistence/     InMemoryRunStore, SqlRunStore (Postgres), bootstrap seed
  telemetry/       Tracing / persistence / MLflow event sinks
  dsl/             YAML → Specification
  plugins/         Built-in capability registration
  graphs/          Specialist catalog, long prompts, DAG runner, agent tools
  api/             FastAPI + WebSocket control plane + console auth
ui/frontend/       React control console (Vite)
docs/architecture/ architecture.md
tests/             Engine, graphs, tools, API, bootstrap
docker-compose.yml Postgres 16
Contributing

Five rules the architecture won't bend on.

Defects are welcome as issues. The architecture document itself is frozen — implementation and capabilities are where change belongs.

Keep ControlSystem as the aggregate root — never promote Graph or Loop to product identity.
Controllers stay topology-blind.
Events remain append-only; signals remain immutable.
New behavior ships as a capability, not a special case in ExecutionEngine.
Postgres is the production RunStore — no SQLite as a product backend.
on tokens

Live calls take prompt and completion tokens from the provider via LiteLLM. If an endpoint returns zeros, Actuate falls back to a length-based heuristic so budgets still move. Graph totals sum every specialist and judge call, including tool rounds. This is not a billing-grade tokenizer.

Maintainer

Built and maintained by one engineer, in the open.

EN

Ezhilan Nagarajan

Software engineer building an automated loan-review pipeline for correspondent business loans at Rocket India, and maintainer of actuate-ai in his own time. Focused on full-stack, cloud-native, event-driven, and AI-powered systems.

Chennai, India Chennai Institute of Technology maintainer of actuate-ai
Get involved

Star it if the control-systems framing is useful. Fork it to try a different controller.

Open issues for defects. The architecture document is frozen — implementation is where change belongs.

clone
git clone https://github.com/actuate-ai/actuate.git
fork
git clone https://github.com/<you>/actuate.git
git remote add upstream https://github.com/actuate-ai/actuate.git