CLI
Every af command: init, run, up, ps, down, validate, flags, schema, discord-invite.
af is the framework's CLI, published to JSR as @looped/af. On macOS or Linux, install it with Homebrew — no Deno required:
brew install loopedautomation/tap/afOr install from JSR with Deno:
deno install -g --allow-read --allow-write --allow-env --allow-net --allow-run=bash,docker,deno -n af jsr:@looped/afEither way the CLI drives Docker, which must be installed and running. Upgrade a Homebrew install with brew upgrade af; a Deno install with af update.
The CLI orchestrates Docker under the hood: agents always execute in the published container, never on your machine. Pointing af at an agent file starts the container with the config mounted, the data volume attached and the env file passed. Config paths default to ./agent.yaml.
af init [name] Scaffold a new agent project (agent, secrets, deployment)
af run [agent.yaml] Run one agent in Docker, interactive (REPL without triggers)
af up [agent.yaml...] Start agents in Docker — foreground; -d to detach
af ps List af containers
af down [target...] Stop and remove af containers (files or handles; none = all)
af validate [agent.yaml] Validate an agent definition
af flags [agent.yaml] Print the Deno sandbox flags this agent runs under
af schema Print the agent.yaml JSON Schema
af discord-invite <agent.yaml>
Print the bot's OAuth invite URL (no bitfield math)
af version Print the af version (also --version, -v)Under the hood
Every af command that runs an agent expands to a plain docker run on the published base image — no daemon of its own, no state outside Docker. af up -d agent.yaml is exactly this:
docker run -d --restart unless-stopped \
--name af-agent \
--label af.agent=agent \
-v ./agent.yaml:/agent/agent.yaml:ro \
--env-file .env \
-v agent-data:/data \
-p 127.0.0.1:0:9090 \
--read-only --tmpfs /tmp \
ghcr.io/loopedautomation/agent:0.12.0That's the config and any skills: mounted read-only, the <handle>-data volume so identity survives restarts, the .env next to the agent file, the status surface on an ephemeral loopback port so fleets never collide, and a read-only root filesystem. The image tag matches the CLI's version — never :latest, so a cached image can't drift out from under a newer CLI (--image overrides). af ps and af down find containers by the af.agent label. Because it's all plain Docker, everything you know still works: docker logs af-<handle>, docker stats, restart policies, volume backups.
--dry-run on run/up prints the exact command for your agent instead of executing it — useful for pasting into a systemd unit or a runbook.
af init
af init scaffolds a complete agent project into <name>/. It's interactive by default, and every question is also a flag, so you can script it as one line:
af init issue-helper --trigger discord --provider openai-compatible \
--deploy compose --clis gh| Flag | Choices | |
|---|---|---|
--trigger | discord webhook cron none | none = REPL agent |
--provider | openai-compatible anthropic codex local | codex = ChatGPT subscription via codex login, no key; local = openai-compatible + Ollama base_url, no key |
--deploy | local docker compose compose-inline paas-git paas-env | see below |
--model | any model id | sensible default per provider |
--clis | comma-separated executables | adds a Dockerfile layer + permissions.run |
--handle / [name] | letters, digits, hyphens | what you call the agent |
--dir | a directory | where to scaffold (default .) |
Every shape generates agent.yaml, .env.example (every secret the config references, ready to copy to .env) and a README.md with the exact deploy steps. Each deploy shape then adds its own files:
local- nothing more; youaf validateandaf run.docker- thedocker runcommand in the README, with the mounted config, env file and data volume.compose- acompose.yaml, aDockerfilewhen--clisneeds one and a.gitignore.compose-inline- a singlecompose.yamlwith the agent config defined inline in a top-levelconfigs:element and mounted at/agent/agent.yaml.paas-git- aDockerfileandcompose.yamlfor platforms that build from a repo (Coolify, for example): you push, connect, set the env vars and deploy.paas-env- for platforms where a deploy is an image plus env vars: the stock image with the config inAF_AGENT_CONFIGand no files at all.
af run
af run starts one agent in Docker, interactive and in the foreground (docker run -it --rm): a long-lived service if the config has triggers: and a REPL if it doesn't. On first boot the agent picks its name and prints the birth banner. Ctrl-c stops the container; the <handle>-data volume stays, so the identity survives.
Inside the published image the entrypoint calls the same command with AF_CONTAINER=1 set, which makes run execute the agent in-process — that branch is the container entrypoint. Set AF_CONTAINER=1 yourself only for framework development.
af up
af up starts one container per agent file. The foreground default streams every agent's logs with a colored [handle] prefix and stops them all on ctrl-c; -d detaches with --restart unless-stopped and waits until each agent answers on its status port:
$ af up -d issue-bot/agent.yaml helpdesk/agent.yaml
✓ issue-bot running · status http://127.0.0.1:55031
✓ helpdesk running · status http://127.0.0.1:55047
af ps to inspect · af down to stopFor every agent it mounts the config and any skills: read-only, attaches the <handle>-data volume, passes the .env next to the agent file (--env-file overrides), publishes the status surface on an ephemeral loopback port and webhook trigger ports directly, and runs the container --read-only. --image overrides the image; --dry-run prints the docker command(s) and starts nothing. A handle that already has a container fails loudly — af down it first.
af ps
af ps lists the af-managed containers (running or stopped, discovered by the af.agent label) with their state, uptime and status-surface address.
af down
af down gracefully stops and removes af containers — pass agent files or handles, or nothing for all of them. Data volumes are never removed: a lost volume is a fresh identity.
af validate
✓ agent.yaml is a valid agent definition
handle: issue-bot
model: openai-compatible / gpt-5.4-mini
triggers: discord
sandbox: net open below the app layer; egress bounded by the container
⚠ permissions.run grants gh — a subprocess leaves the Deno sandbox
env refs: OPENAI_API_KEY, DISCORD_BOT_TOKEN, GITHUB_TOKEN
⚠ not set in this environment: GITHUB_TOKENaf validate parses the config (unknown keys are hard errors) and prints the identity, the triggers, every env var the config references (with a warning on any that aren't set in the environment) and which sandbox the agent gets.
This one granted gh, so it gets the container as its egress boundary. An agent that spawns nothing runs hermetic instead, and af validate lists the hosts it's allowed to reach:
sandbox: hermetic — Deno enforces net for the whole process
egress: 0.0.0.0:9090, api.anthropic.com, api.github.comaf flags
af flags prints the Deno permission flags the agent runs under in the container, which is layer 1 of the sandbox.
An agent that spawns nothing runs hermetic: its net allowlist, plus the hosts the framework works out from the config, compiled into --allow-net.
$ af flags agent.yaml
--allow-env --allow-read=/agent,/skills,/data,/looped,/deno-dir,/run/secrets \
--allow-write=/data --allow-net=0.0.0.0:9090,api.anthropic.com,api.github.comIf the agent has a permissions.run grant or a stdio MCP server, then a subprocess is going to leave the Deno sandbox and the runtime can't hold its network access. You get the image's flags instead, and the reason why on stderr.
af schema
af schema prints the agent.yaml JSON Schema. It's the same schema the runtime enforces and the one editors validate against.
af login / af deploy / af agents / af status
These commands drive Looped Agents, the hosted platform. In v1 the platform deploys GitHub-connected agents only: you connect a repo once in the dashboard (a GitHub App install), and from then on the platform deploys whatever is on the connected branch. af deploy is the terminal end of that flow — it never uploads your working tree.
af login [key] verifies and stores a team API key (minted in the dashboard with the read:agent and write:agent scopes) in ~/.config/looped/credentials.json. In CI, set LOOPED_API_KEY instead; LOOPED_API_URL overrides the gateway for staging.
af deploy [agent.yaml] [--agent <handle>] matches your checkout to a hosted agent — by the origin remote's owner/repo, then the current branch, then the config's handle when one repo hosts several agents — triggers a deploy of the branch head, and polls until the deployment is healthy or failed (surfacing the platform's boot error on failure). It warns when the working tree is dirty or the branch has unpushed commits, because those won't be in the deploy. --agent <handle> picks the target explicitly.
af agents lists the team's hosted agents with their connected repos. af status [agent.yaml] [--agent <handle>] prints the matched agent's live machine status without waking a sleeping agent.
af discord-invite
af discord-invite prints the bot's OAuth invite URL with the correct scopes and permissions (View Channels, Send Messages and Read Message History), so you don't have to do the bitfield math yourself. It needs the config, to find the Discord trigger's token_env, and that token set in the environment; it looks up the application id from the token. It's part of the Discord setup.
Are you an AI? Visit llms.txt — these docs as plain markdown.