# Architecture

> Markdown mirror of /docs/architecture.html

At runtime, Loopy is one engine process plus a fresh sandbox for each step's agent. This page walks through both, and the outside services they depend on.

## Loopy engine

The running system has two halves. The **engine** (`loopy run`) is one ordinary process: it receives webhooks, turns them into events, and decides which steps run next. The **agents** do the actual work, each in its own sandbox, away from the engine. When something happens outside (a Sentry alert, a push to GitHub) it arrives as a webhook and a sensor turns the payload into an event. A cron trigger skips that path: on schedule, the engine builds the tick event itself. Either way, the engine finds the steps that listen for the event, and each of those steps gets an agent in a fresh sandbox.

```
External sources (your signals)
  Sentry · Linear · Datadog · GitHub
        |
        v   HTTPS POST /hooks/…
The engine
  SensorRunner    Runs your @sensor code: a raw payload becomes a registered event.
  EventReceiver   Re-validates the event against the registry, then publishes it.
  EventBus        Routes events to every on: subscriber. The one piece you can swap out.
  WorkflowRunner  Runs steps in after: order, fills in their templates, records history.

  On one machine the bus lives inside the process; point it at redis to receive
  webhooks on one machine and run steps on another. Run history is written to SQLite.
  For each step, the AgentHarness fills in the step's instructions, then starts the
  agent in the sandbox below.
        |
        v   per step, the AgentHarness launches the agent
Sandbox
  The agent CLI runs here, isolated from the engine. The agent's model calls and
  tool side effects (git push, open a PR) all originate here. Egress is open unless
  the sandbox sets a network: allowlist.
```

### How it runs today: one machine, plus sandboxes

Today Loopy is built to run on a single machine. Everything in the diagram except the sandboxes lives inside the one `loopy run` process: the SensorRunner, EventReceiver, EventBus, Scheduler, Runtime, and the per-step AgentHarness are all part of it, and the StateStore (run history) is a SQLite file next to it. There is one process to start, one to watch, and one file of history.

The agents are the only part that runs somewhere else. Each step gets its own fresh sandbox (Daytona by default), so the model calls and the `git` work happen away from the engine, with egress open unless the sandbox sets a `network:` allowlist. Secrets are injected into the sandbox, never into the engine or the compiled manifest. The engine decides what runs; the sandboxes do the running. Two services sit outside both and are reached from inside the sandbox: the model API (Anthropic or OpenAI) and GitHub.

Growing past one machine is a swap, not a rewrite. Point the EventBus at Redis, and webhooks can land on one machine while workers on other machines pick up steps from the stream. The sandboxes were already elsewhere, so nothing else moves, and no workflow `.md` changes.

## The services Loopy relies on.

Loopy itself is the engine that compiles your project and drives runs. It leans on a few external services for the parts it doesn't run.

**Daytona**

The default sandbox: a container cloud where each step's agent runs, isolated and built from the `image:` spec in `registry.yml`. You reach it with `DAYTONA_API_KEY` in `loopy.env` (`DAYTONA_API_URL` is optional and defaults to prod). To run agents locally instead, switch a sandbox's `provider:` to `docker` (hermetic, same image spec) or `local` (a bare subprocess).

**Anthropic**

The model behind the default `claude-code` runtime. Set `ANTHROPIC_API_KEY` in the sandbox's `env_file` (`secrets/base.env`); it is injected into the sandbox at runtime, not held by the engine. The scaffold's default model for the `claude-code` runtime is `claude-opus-4-8`, set per agent in `registry.yml` (for example on an agent named `Claude`).

**OpenAI**

The model behind the `codex` runtime, an alternative to `claude-code`. A codex agent (say `Codex`) sets a `gpt-*` model such as `gpt-5.5`; put `OPENAI_API_KEY` in the same sandbox `env_file`. An agent names exactly one runtime, so a project can mix both by referencing a `claude-code` agent (`Claude`) on some steps and a `codex` agent (`Codex`) on others.

**GitHub**

Where agents clone, push, and open PRs. The recommended path is a GitHub App you create with the [manifest flow](/docs/get-started.md#github): the engine mints a short-lived, repo-scoped token per step and injects only that token into the sandbox. A plain `GITHUB_TOKEN` (a PAT) in the sandbox `env_file` also works if you skip the App.

Where each credential lives is the whole point of the split: `loopy.env` holds the engine's infrastructure creds (Daytona, the GitHub App key), and the sandbox `env_file` holds the agents' creds (the model key). See [Secrets](/docs/get-started.md#secrets).
