Carrier
For AI generation

Precise enough for humans. Constrained enough for agents.

Carrier’s advantage isn’t maximal language power. It’s that the backend surface is small, explicit, and compiler-checked. That is exactly the shape that lets code generators stay consistent across a project — and across drafts.

Why it fits

Six properties that matter

Narrow syntax
Twenty top-level declarations. One service. A tight scalar set plus Vector(N). Fewer ways to be wrong.
Authoring tiers
Docs grade constructs: Tier 1 first-pass-safe, Tier 2 recommended, Tier 3 advanced runtime. Start at the smallest tier that fits.
First-class manifest
Every build emits .carrier/manifest.json: models, routes, policies, jobs, schedules, clients, workflows, queues, tenants, flags, llm_clients. Tools read it instead of scraping.
Compiler feedback loop
carrier check runs the full semantic pass — types, CRUD validity, auth/policy rules, idempotency scope, workflow dependency graphs, LLM tool schemas.
Predictable layout
Numeric-prefixed multi-file layout (00_models, 10_types, 20_actions, 30_routes) keeps generation order stable.
Project-local agent docs
carrier agent-docs writes AGENTS.md and CLAUDE.md into the project so Claude Code and Codex start with grounded, project-specific guidance.
For agents AND for agent-built services

LLM clients belong inside the compiler contract

When a Carrier service calls an LLM, the tool surface is typed. Tool parameter schemas come from Carrier types. Tool dispatch reapplies auth and policy context. Every tool call is audited. Transcripts persist in carrier_llm_conversations.

src/support/agent.carrier
llm client · tool · respond_as
fn search_help_docs(term: String) -> String[] {
return [term, "billing", "password reset"]
}
action get_support_profile() -> MeResponse {
let actor = auth.current_user()
return { id: actor.id, email: actor.email, name: actor.name }
}
llm client SupportAgent {
provider: env("LLM_PROVIDER", "openai")
wire_format: env("LLM_WIRE_FORMAT", "openai")
model: env("LLM_MODEL", "gpt-4.1-mini")
api_key: env("LLM_API_KEY", "replace-me")
max_turns: env_int("LLM_MAX_TURNS", 8)
temperature: 0.2
system_prompt: "You are a concise support assistant."
tool search_help_docs(term: String) -> String[] = search_help_docs
tool get_support_profile() -> MeResponse = get_support_profile
}
route POST "/support/draft" protect Auth -> SupportTicketDraft {
input: SupportReplyRequest
handler {
return SupportAgent.respond_as(SupportTicketDraft, {
user_prompt: input.message
conversation_id: input.conversation_id
})
}
}
Typed structured output
respond_as(TypeName, ...) returns a value matching the named Carrier type directly — no loose JSON, no manual schema glue.
Grounded tools
Tool targets are Carrier fn or action declarations. Tools run inside the same auth/policy session as the triggering route.
Audited + observable
Every tool call writes audit.record("llm_tool_call", ...) and emits OTLP spans with carrier.client_name and token attributes.
Project-local guidance

carrier agent-docs — the one command to run first

Before an agent writes a line of Carrier, run carrier agent-docs inside the project. Carrier writes AGENTS.md and CLAUDE.md so Codex and Claude Code start from project-specific, grounded guidance — not generic web-scraped patterns.

$ cd my-service
$ carrier agent-docs # AGENTS.md + CLAUDE.md
$
# now Claude Code / Codex have grounded project context
$ claude 'add an idempotent POST /orders route'
Why this matters
Agents drift when they guess at a stack. Running agent-docs pins the Carrier vocabulary, the multi-file layout, the CLI, and the compile/check loop inside the repo itself.
Docs for agents

The generation recipe, verbatim from docs/ai-authoring.md

This is the order the AI authoring guide recommends. It matches what the compiler expects in practice.

docs/ai-authoring.md
0.9 generation recipe
1. write carrier.toml
2. declare one service (with telemetry if OTLP is available)
3. add auth jwt if any route is protected
4. create enum used by models or query defaults
5. create model declarations (include Vector(N) when retrieval is in scope)
6. create type declarations for I/O and pagination
7. add crud for resource-like models
8. add policy for role or tenant visibility
9. add action for real business writes
10. add custom route
11. add workflow when a flow has durable steps / compensation / parallel branches
12. add job / event / schedule / queue only when needed
13. add tenant when tenant lifecycle matters
14. add flag for percentage / tenant-scoped rollouts
15. add stream / subscription / watch for realtime
16. add client for outbound JSON integrations
17. add llm client for typed LLM tools
18. add pure fn helpers last
19. add test blocks for the public route contract
Tier 1 — first-pass-safe
one service · optional auth jwt · type · route · optional model + crud · pure fn helpers
Tier 2 — recommended
multi-file layout · action with transactions · idempotent write routes · policy where role visibility matters · test blocks
Tier 3 — platform
cache · jobs · schedules · queues · events · audit · raw SQL · DB functions · workflow · llm client · Vector(N) · tenant RLS · OTLP telemetry
Honest

What NOT to invent

Carrier is grounded, not aspirational. The AI authoring guide is explicit about what does not exist yet.

No
Language features not present in the repository — no trait system, no generics, no macros, no custom decorators, no package registry.
No
Framework magic — Carrier does not hide behavior behind annotations. Everything visible compiles into the expected output.
No
Built-in embedding providers. Wrap your embedding source in a typed native fn or accept Vector(N) from upstream.
No
Mixed graph + compensation in one workflow yet — saga rollback still requires the linear execution model. Use explicit after StepName for dependencies.
No
return inside transaction { } — do the work in the block, return after.
No
LLM generic syntax — use respond_as(TypeName, ...) instead of parameterised type arguments.