The 30 MB open-source edge AI agent runtime. Run AI agents offline, on Linux.

Build an edge agent visually, deploy it to a Raspberry Pi, and let it talk to GPIO, MQTT and local SLMs — no cloud required.
Offline by default. GPIO, UART, MQTT as first-class nodes. Local SLMs alongside cloud LLMs in the same workflow. Industrial protocols (OPC-UA, Modbus) are on the roadmap.
Runs on Linux edge devices: Raspberry Pi 5 · Jetson Orin Nano · STM32MP25 · Bosch Rexroth ctrlX CORE.
⭐ Star the repo if you think AI agents belong beyond the cloud.
Today's AI agents live in datacenters. The interesting workloads — sensors, machines, vehicles, gateways — live everywhere else. Edge Agents brings the agent paradigm to the devices that interact with the real world: small enough to run on a Pi 5, capable enough to drive an industrial controller, with hardware I/O as native primitives instead of REST shims.
| Edge Agents | n8n | LangGraph | Dify | OpenClaw | |
|---|---|---|---|---|---|
| Runtime size | ~30 MB container | ~500 MB Docker | Python library | ~500 MB Docker | ~1 GB Docker |
| Offline by default | ✅ | ❌ | depends on host | ❌ | ❌ datacenter-only |
| Hardware I/O (GPIO, UART, ADC) as nodes | ✅ first-class | ❌ | ❌ | ❌ | ❌ |
| On-device SLM provider | ✅ typed multi-endpoint | ❌ | partial via libs | ❌ | ❌ |
| MQTT as workflow transport | ✅ first-class | community node | ❌ | ❌ | ❌ |
| Visual builder | ✅ | ✅ | ❌ code-only | ✅ | ❌ |
| Industrial protocols (OPC-UA, Modbus) | on roadmap | community nodes | ❌ | ❌ | ❌ |
Two pieces: the engine (a small container that runs your workflows) and the
fh-workflow CLI (authors, validates, and visually edits workflow files). You can
run the engine without ever cloning this repo, and author workflows with a single
npm i -g @foresthubai/workflow-cli.
The lightest path needs no clone and no Docker — just the CLI and the visual builder:
npm i -g @foresthubai/workflow-cli
fh-workflow open my.workflow.json # opens the visual builder; Save writes back to the file
Don't have a workflow yet? Let Claude Code write one from a single sentence — see Generate workflows with Claude Code. Ready to run it on real hardware? Run the engine on the device.
The engine ships as a small container you build from go/Dockerfile
(multi-arch, distroless, nonroot). Most edge targets are arm64 (Pi, Jetson, STM32MP2,
ctrlX), so the common flow is to cross-build on an amd64 workstation and ship the
result to the device:
cd go
# Cross-build for an arm64 edge device (use --platform linux/amd64 for x86 targets)
docker buildx build --platform linux/arm64 -t fh-engine:latest --load .
# Ship to an offline device: save to a tar, copy it across, load it there
docker save fh-engine:latest -o fh-engine.tar
# scp fh-engine.tar device:/tmp/ ← then, on the device:
docker load -i fh-engine.tar
# The engine boots exactly one workflow, read once from ENGINE_CONFIG_FILE.
# A `fh-workflow deploy` bundle wires this up for you (see "Deploy a workflow").
docker run --rm \
-v "$PWD/engine-config.json:/etc/foresthub/engine-config.json:ro" \
-e ENGINE_CONFIG_FILE=/etc/foresthub/engine-config.json \
fh-engine:latest
Building for the same architecture you're already on? A plain
docker build -t fh-engine:latest . works too — the Dockerfile cross-compiles via
TARGETARCH, so QEMU only emulates the trivial copy into the final layer.
The fh-engine:latest tag is the one a fh-workflow deploy bundle expects (its
docker-compose.yml loads the image with pull_policy: never), so an image built here
drops straight into a generated bundle.
The engine is a headless, immutable runner — it serves no inbound HTTP. It reads its
single boot config (workflow + bindings + device manifest) once from ENGINE_CONFIG_FILE,
runs that one workflow, and exits when the workflow does; a boot failure exits the process.
It runs standalone by default — no control plane, no account, no inbound port, no
outbound calls beyond LLM provider APIs. Setting FH_BACKEND_URL (with ENGINE_SECRET)
opts into outbound log shipping and memory sync only; liveness is observed externally from
the container's exit, not self-reported. Configure via ENGINE_* env vars; see
go/cmd/engine/config.go.
Hardware access: the image runs as a nonroot distroless user, so reaching real GPIO,
serial or analog devices needs them passed into the container with the right group — e.g.
--device /dev/gpiochip0 --group-add "$(stat -c '%g' /dev/gpiochip0)" (or run with
--privileged on a throwaway dev box). Pure-software workflows need none of this.
A workflow is a *.workflow.json file you author, validate, and open in the visual
builder. Install the fh-workflow CLI from npm — no clone required:
npm i -g @foresthubai/workflow-cli
# or run it without installing:
npx @foresthubai/workflow-cli <command>
fh-workflow open my.workflow.json # open the visual builder; Save writes back to the file
fh-workflow validate my.workflow.json # semantic: wiring, references, types
fh-workflow check-schema my.workflow.json # structural: types, required fields, enums
fh-workflow update my.workflow.json # migrate a workflow to the current schema version
fh-workflow deploy my.workflow.json # generate a self-contained deployment bundle
fh-workflow help # list all commands
fh-workflow open is the visual builder — the same React Flow canvas, served locally;
hit Save and it writes straight back to your file. See ts/workflow-cli
for the full command reference and the --static / --dev open modes.
Describe a workflow in plain language and the workflow-generate skill writes the
*.workflow.json and runs the validators for you. Install it into any project with the
skills CLI — no clone required:
npx skills add ForestHubAI/edge-agents --skill workflow-generate
The skill validates by shelling out to the fh-workflow CLI, so install that too
(npm i -g @foresthubai/workflow-cli). Then just describe a workflow — e.g.
"read a sensor every 10s and toggle a relay" — and the skill generates and validates
the file for you.
The quick path is fh-workflow deploy my.workflow.json — it inspects the workflow,
asks for the values it can't infer (device paths, broker URLs, model files, API keys),
and writes a self-contained bundle: docker-compose.yml, .env, the workflow, and any
config files the workflow needs — plus a README.md with the build/transfer/run steps.
For an on-device SLM it even drops in a llama.cpp sidecar wired to the engine over the
compose network. Without a terminal (CI, a Claude Code skill) feed the answers with
--values <file.json>. The rest of this section explains what ends up in that bundle —
and how to assemble it by hand if you'd rather.
Prefer to drive it from Claude Code? The workflow-deploy skill runs this same flow —
reading the workflow, gathering the values, and writing the bundle while keeping secrets as
placeholders. Install it the same way:
npx skills add ForestHubAI/edge-agents --skill workflow-deploy
A workflow is binding-free: it declares what it needs — channels (GPIO, MQTT, …)
and custom models — but not where those live on a given device. You supply the where
through a few small config files mounted into the engine container alongside the
workflow. See go/docs/workflow-deployment-layers.md for the file
schemas and deploy-time validation rules.
What the engine reads, and when each file is needed:
| File | Engine env var | When you need it |
|---|---|---|
| workflow JSON | ENGINE_CONFIG_FILE |
always — the graph itself |
| device manifest | ENGINE_DEVICE_MANIFEST_FILE |
only with hardware channels (GPIO / ADC / DAC / PWM / UART) — maps a logical id to a physical /dev/… |
| external resources | ENGINE_EXTERNAL_RESOURCES_FILE |
only with MQTT channels or custom/self-hosted models — broker connections and LLM endpoints |
| deployment mapping | ENGINE_DEPLOYMENT_MAPPING_FILE |
as soon as any channel or custom model exists — binds each logical id to a resource (+ index for hardware) |
Rule of thumb: a workflow with no channels and only built-in catalog models (e.g.
claude-haiku-4-5) needs none of the extra files — just the workflow JSON and the
provider's API key. Add the mapping the moment a channel or a custom model appears; add
the device manifest for hardware, external resources for MQTT and self-hosted models.
To assemble this by hand instead of with fh-workflow deploy: ship the image with the
docker save / docker load flow from Run the engine and start it
with docker run, mounting the files above with -v and pointing the ENGINE_* env
vars at them. The repo ships no static compose.yaml; fh-workflow deploy generates one
per workflow, or write your own.
llama.cpp / vLLM / Ollama / any OpenAI-compatible endpoint.contract/*.yaml for both Go and TypeScript; CI fails on schema drift.Models run on the device through llama.cpp, in their own container, separate from
the engine. Reference the model in the workflow as a custom LLMModel; the engine
talks to its endpoint over HTTP.
The easy way is to mark that model as on-device and let fh-workflow deploy add the
llama.cpp sidecar to the generated docker-compose.yml for you — reached by service
name over the compose network, no host networking, no hand-written compose (see
Deploy a workflow to a device).
To run the inference container by hand instead — pull its server image and point it at a
.gguf model file (e.g. a quantized Gemma):
docker pull ghcr.io/ggml-org/llama.cpp:server-b8589
docker run --rm --network host -v "$PWD/models:/models:ro" \
ghcr.io/ggml-org/llama.cpp:server-b8589 \
--model /models/gemma-3-270m-it-Q4_0.gguf --host 0.0.0.0 --port 8090
Start it before the engine, then point the workflow's custom model at this endpoint through the deploy files (again, Deploy a workflow to a device).
go-gpiocdev (digital in/out, edge triggers)go.bug.st/serialDigital and analog signal types are first-class in the workflow contract.
✅ = brought up and exercised on our own bench. We don't yet publish a per-device CI matrix, so treat these as known-good targets rather than a continuously tested guarantee.
| Target | Status |
|---|---|
| Raspberry Pi 5 (8 GB) | ✅ |
$ claude mcp add edge-agents \
-- python -m otcore.mcp_server <graph>