Carrier
Language reference

Thirty-three declarations. A compiled, AI-friendly surface for fast, secure APIs.

Thirty-three top-level declarations, a scalar set plus Vector(N) and Decimal, and a tight runtime-bound built-in surface. Enough to build a typed, role-aware, RLS-backed API with workflows, agents, RAG, vector search, queues, realtime, native PDF, MCP, formal invariants, first-class SLAs, and Tank-hardened ops routes — all grounded in the 1.3.4 release candidate.

compiledtypedexplicit

Top-level declarations

Thirty-three declarations. Every Carrier program is composed from this set. No imports bring in new grammar; no macros hide behavior.

service
API surface · telemetry
runtime
Tank limits · ops_routes · panic
auth jwt
JWT · JWKS · role hierarchy
import
modules · registry refs
enum
named variants
type
request/response records
model
persisted records · Vector(N)
crud
generated resource endpoints
fn
pure helpers · native fn
action
business logic · transactions
policy
role/tenant visibility · RLS SQL
route
HTTP endpoints · idempotent
client
typed JSON · gRPC unary
llm client
typed LLM tools · respond_as · routed
agent
chat-thread participants
rag
retriever + LLM in one decl
workflow
steps · sagas · graph · retry
sla
attainment · deadline · escalation
invariant
must_always · must_never · transition
event
durable domain events
job
async work units
schedule
cron-driven jobs
queue
Redis · consumers · DLQ
tenant
lifecycle · retain N days
flag
percentage · tenant rules
ui
compiler-mounted Yew shell
api_explorer
typed Try-it surface
federated service
imported manifests
materialized_view
governed precomputed reads
analytics.report
typed analytics outputs
data_export
tenant-scoped exports
eval
graded LLM regression checks
finetune
curated training datasets

Types & annotations

Carrier has named records, enums, arrays, optionals, and a tight scalar set. Validation and DB metadata attach with @-annotations.

  • StringUTF-8 string
  • Int · Floatchecked i64 / 64-bit floating point
  • Boolboolean
  • UUIDUUID
  • Time · Datetimestamp · calendar date
  • Json · Voidarbitrary JSON · no value
  • Vector(N)pgvector-backed embedding
  • T[]array of T
  • T?optional T
  • @length(min, max)string length bounds
  • @range(min, max)numeric bounds
  • @emailemail format validation
  • @primary · @uniqueidentity · uniqueness
  • @indexDB index
  • @belongs_to(Model)relation metadata
  • @versionoptimistic concurrency field
src/inventory/00_models.carrier
model · policy · crud
enum InventoryStatus { active archived }
policy InventoryItem {
write: roles [operator]
deleted: roles [operator]
}
model InventoryItem {
id: UUID
sku: String @length(min: 3, max: 80)
name: String @length(min: 3, max: 120)
warehouse: String
available_units: Int @range(min: 0, max: 1000000)
safety_stock: Int @range(min: 0, max: 1000000)
status: InventoryStatus = active
deleted_at: Time?
created_at: Time
updated_at: Time
}

Modules & recipes

Carrier 1.0 ships a real module + registry pipeline. Bare imports, registry refs with semver, locked checksums + provenance + SBOM, and recipe metadata for vertical solution bundles travel through the same artifact.

carrier.toml
modules · registry refs
# carrier.toml
[package]
name = "acme/clinic-app"
version = "0.1.0"
[modules]
"walknorth/forms" = { registry = "../carrier-registry", version = "^1.2" }
"walknorth/analytics" = { registry = "../carrier-registry", version = "^2.1" }
"walknorth/fhir-patients" = { registry = "../carrier-registry", version = "^0.4" }
carrier.toml
recipe · analytics_pack
[package]
name = "acme/clinic-operations"
version = "0.1.0"
[recipe]
summary = "Clinic operations starter built on Carrier modules"
positioning = "Patient intake, scheduling, and weekly ops reporting"
vertical_tags = ["healthcare", "clinic", "operations"]
required_capabilities = ["postgres", "email"]
default_analytics_packs = ["weekly-clinic-ops"]
[[recipe.modules]]
name = "acme/forms"
version = "^1.4"
role = "included"
description = "Patient intake and consent flows"
[[recipe.analytics_packs]]
name = "weekly-clinic-ops"
vertical = "clinic"
business_roles = ["front_desk_manager", "practice_admin"]
metrics = ["no_show_rate", "provider_utilization", "appointment_lead_time"]
reports = ["appointments_by_provider", "no_show_rate", "intake_backlog"]
required_models = ["Appointment", "ProviderSchedule", "PatientEncounter"]
[recipe.compatibility]
carrier = "^1.0"
Install
carrier install resolves the highest non-revoked semver version, verifies signature + provenance + SBOM, and writes everything to carrier.lock.
Audit
carrier audit --format json --fail-on high checks registry advisories + OSV.dev. CI-friendly machine output.
Recipe scaffold
carrier new app --recipe acme/clinic-operations seeds a workspace from the highest published recipe version.

Service & auth

One service per project. JWT auth configures issuance, verification, and role-aware route protection.

examples/hello-carrier/src/main.carrier
Tier 1 · hello-carrier
service App {
openapi { title: "Hello Carrier API" version: "0.1.0" }
server { host: env("HOST", "0.0.0.0") port: env_int("PORT", 3000) }
}
auth jwt Auth {
issuer: env("JWT_ISSUER", "carrier")
audience: env("JWT_AUDIENCE", "carrier-users")
secret: env("JWT_SECRET", "local-dev-secret")
}
type RegisterRequest {
email: String @email
name: String @length(min: 2, max: 100)
password: String @length(min: 8)
}
type AuthTokens { access_token: String refresh_token: String }
route POST "/auth/register" public -> AuthTokens {
input: RegisterRequest
handler {
let user = auth.register(
email: input.email, name: input.name, password: input.password
)
return auth.issue_tokens(user.id)
}
}
route GET "/me" protect Auth -> MeResponse {
handler { return auth.current_user() }
}

Auth role hierarchy & JWKS

auth jwt blocks accept a roles { ... } table that lets a role include other roles. Routes can require ALL of a list with all_roles or ANY (the default with roles). JWKS verification is opt-in via jwks_url.

src/auth.carrier
auth jwt · roles { includes } · all_roles
auth jwt AdminAuth {
issuer: env("JWT_ISSUER", "carrier")
audience: env("JWT_AUDIENCE", "carrier-users")
secret: env("JWT_SECRET", "local-dev-secret")
jwks_url: env("JWT_JWKS_URL", "")
roles_claim: "roles"
roles {
super_admin includes [admin]
admin includes [editor, viewer]
editor includes [viewer]
}
}
route POST "/orgs/{id}/archive"
protect AdminAuth all_roles [admin, compliance_officer]
-> Org
{
params { id: UUID }
handler { return Org.archive(params.id) }
}
Inclusion
admin includes [editor, viewer] grants admin the implicit editor and viewer capabilities for role checks.
Any vs all
Default protect Auth roles [a, b] is any-of. Use protect Auth all_roles [a, b] when both must be present.
JWKS
Set jwks_url to verify tokens against a remote signing-key set. The runtime caches the JWKS and respects standard expiry hints.

service.runtime · Tank limits

A first-class runtime block under service that pins request/action/route/workflow depth, JSON body depth, schema expansion depth, body and artifact size, request timeout, panic policy, evidence emission, and ops-route exposure. Generated services return CARRIER_DEPTH_LIMIT_EXCEEDED with structured limit/location/hint metadata instead of overflowing the stack.

src/main.carrier
runtime · panic_policy · ops_routes
service App {
openapi { title: "Tank-Hardened API" version: "1.0.0" }
runtime {
request_depth_limit: 32
action_depth_limit: 16
workflow_depth_limit: 16
expression_depth_limit: 64
json_body_depth_limit: 16
schema_expansion_depth: 16
body_size_limit: 2_megabytes
artifact_size_limit: 50_megabytes
request_timeout_ms: 5_000
panic_policy: catch
evidence: emit_when_explicitly_enabled
ops_routes: local_only
}
}
Depth guards
Carrier counts request/action/route/workflow depth and rejects cycles before the stack overflows. Limits are configurable per-service and surface a structured diagnostic.
Panic capture
panic_policy: catch wraps execution spans so generated services keep serving traffic after a panic. The captured event lands in the in-process Carrier Flight Recorder.
ops_routes
/ops/runtime and /ops/flight are emitted with policy: off, local_only (default), or authenticated.

Models & CRUD

crud generates list · get · create · update · delete · restore for any model, with pagination, filters, sort, search, and soft-delete scopes.

api/src/main.carrier
generated endpoints
crud Doctor at "/doctors" {
create: protect AdminAuth roles [admin]
list: public
get: public
update: protect AdminAuth roles [admin]
delete: soft using deleted_at protect AdminAuth roles [admin]
restore: true protect AdminAuth roles [admin]
list_defaults {
page_size: 20
sort: "rating"
direction: desc
scope: active // active · all · deleted
}
filters {
specialty: exact
city: exact
language: contains
rating: min
}
searchable { full_name specialty city bio }
}
Filter modes
exact, contains, and min today.
Scopes
List/get default to active. Internal callers can pass all or deleted.
Audit
Generated create · update · delete · restore record audit entries automatically.

Routes

Explicit HTTP. Typed path params, query blocks, input bodies, response types, auth forms, and optional idempotency.

api/src/main.carrier
route GET "/doctors/{id}" public -> Doctor {
params { id: UUID }
handler { return Doctor.get(id: params.id) }
}
route POST "/admin/doctors/{id}/lock"
protect AdminAuth roles [admin] idempotent -> Doctor
{
params { id: UUID }
handler { return audit_locked_doctor(params.id) }
}
Auth forms
public, protect Auth, protect Auth roles [admin, editor].
Input
A route may declare any combination of input, query { }, and params { }.
Idempotency
With idempotent, replays with the same Idempotency-Key return the stored body without re-running the handler.

Actions & transactions

Move non-trivial writes out of routes. Actions run with auth context, can open DB transactions, and can lock rows.

api/src/main.carrier
row locking
/// Transaction-safe admin action with row locking
action audit_locked_doctor(id: UUID) -> Doctor {
let actor = auth.current_user()
transaction {
let locked = Doctor.get_for_update(id: id, scope: "all")
logs.info("doctor lock audit", {
action_name: "audit_locked_doctor"
doctor_id: locked.id
actor_email: actor.email
})
}
return Doctor.get(id: id, scope: "all")
}
/// Idempotent admin route; retries return the stored response
route POST "/admin/doctors/{id}/lock"
protect AdminAuth roles [admin] idempotent -> Doctor
{
params { id: UUID }
handler { return audit_locked_doctor(params.id) }
}
Transaction rule
transaction { } opens a real DB transaction. Success commits, failure rolls back. return from inside the block is not supported yet — do the work in the block and return after.

Policies & RLS

policy compiles into manifest metadata and PostgreSQL RLS SQL. The runtime sets role and tenant session values per request.

src/00_models/slots.carrier
tenant-aware policy
policy AppointmentSlot {
tenant_field: org_id
read: roles [viewer, scheduler]
write: roles [scheduler]
deleted: roles [scheduler]
}
model AppointmentSlot {
id: UUID
org_id: String @index
clinician_name: String @length(min: 3, max: 120)
starts_at: Time
status: SlotStatus = open
version: Int @version
deleted_at: Time?
created_at: Time
updated_at: Time
}
Database enforcement
Migrations emit ENABLE ROW LEVEL SECURITY + FORCE ROW LEVEL SECURITY.
Request-time context
Runtime sets carrier.current_roles and carrier.current_tenant on the Postgres session before DB work runs.

Jobs · events · schedules

Durable async edges of the service. One-parameter jobs return Void, events are recorded, schedules poll and enqueue.

src/15_async/jobs.carrier
cron · emit · enqueue
event SlotReminderQueued {
slot_id: UUID
trigger: String
}
job send_slot_reminder(payload: Json) -> Void {
logs.info("slot reminder job", { job_name: "send_slot_reminder", payload: payload })
audit.record("send_slot_reminder", "AppointmentSlot", "scheduled", payload)
}
schedule "*/15 * * * *" run send_slot_reminder
route POST "/slots/{id}/notify"
protect StaffAuth roles [scheduler] idempotent -> Json
{
params { id: UUID }
handler {
let job_id = jobs.enqueue("send_slot_reminder", { slot_id: params.id }, delay_seconds: 5)
emit SlotReminderQueued { slot_id: params.id, trigger: "manual" }
return { job_id: job_id }
}
}

Workflows & sagas

Durable background processes with typed steps, per-step retry overrides, saga-style compensation, and a graph mode with explicit after-dependencies and when-guards for parallel branches.

src/workflows/checkout.carrier
workflow · step · compensate
workflow CheckoutSaga {
input: CheckoutRequest
output: CheckoutResult
timeout_seconds: 300
max_retries: 2
step reserve_inventory -> ReservationSummary
retry {
max_retries: 3
delay_seconds: 10
} {
return Inventory.reserve(input.sku, input.quantity)
}
compensate reserve_inventory {
Inventory.release(reserve_inventory.reservation_id)
}
step charge_card -> ChargeSummary {
if input.simulate_failure {
fail("checkout_failed", "card authorization declined")
}
return Payments.charge(input.payment_method, input.amount_cents)
}
compensate charge_card {
Payments.refund(charge_card.charge_id)
}
step fulfill -> CheckoutResult after charge_card {
return Fulfillment.ship(
reservation: reserve_inventory.reservation_id,
charge: charge_card.charge_id
)
}
return fulfill
}
State
Run rows live in carrier_workflow_state; graph step tasks in carrier_workflow_tasks.
Status enum
Built-in WorkflowStatus: queued · running · retry · completed · failed · compensating · compensated · compensation_failed · timed_out.
Operator recovery
WorkflowName.retry_compensation(run_id) resets a compensation_failed run. No hand-edits to system tables.

SLA declarations

sla blocks attach source-controlled service-level contracts to a workflow with explicit start/end events, attainment %, deadline, escalation, and exclusion rules. Carrier validates the workflow + step references and emits the contract under .carrier/manifest.json slas + OpenAPI x-carrier-slas.

src/slas/sepsis.carrier
attainment · deadline · escalation
sla SepsisBundle {
workflow: AdmitSuspectedSepsis
starts_when: step_started("RecordVitals")
ends_when: step_completed("CompleteBundle")
attainment: 95%
deadline: 4_hours
warning_at: 80%
breach_at: 100%
scope: tenant
measure_by: calendar_month
escalation: notify(role: "charge_nurse") on breach_imminent
compliance_evidence: attach_to_audit
report: include_in("ClinicalQualityDashboard")
exclude_when: field("comfort_care_only") == true
}
Start / end events
Current forms: event("..."), status("..."), step_started("Step"), step_completed("Step").
Attainment & windows
attainment: 95%, deadline: 4_hours, measure_by: calendar_month | rolling_7_days.
Escalation
escalation: notify(role | user | channel | care_team | webhook) on breach_imminent | breached.

Invariants & verification

A focused first slice of compile-time invariant verification. Declare bounded business rules with must_always / must_never / transition. Carrier prefers a real z3 binary on PATH; if absent, a deterministic bounded search keeps the command usable in minimal environments.

src/invariants.carrier
invariant · verification · assume
verification {
solver: z3
mode: bounded
max_records: 1
max_workflow_depth: 10
timeout: 30_seconds
}
invariant InventoryNeverNegative {
subject: InventoryItem
must_always: quantity_on_hand >= 0
}
invariant LabResultRequiresReviewer {
subject: LabResult
transition: status -> "reviewed"
requires: reviewed_by != ""
}
invariant NoProviderOverlap {
subject: Appointment
must_never:
exists other Appointment where
other.provider_id == provider_id &&
overlaps(other.start_slot, other.end_slot, start_slot, end_slot)
}
assume StripeDoesNotOverRefund {
external: stripe.refund
guarantees: refund.amount <= charge.remaining_refundable_amount
}
Verification block
verification { solver: z3, mode: bounded, max_records, max_workflow_depth, timeout }.
Forms
must_always, must_never, transition-style transition: field -> value { requires }, bounded existentials with exists Model where ... and overlaps(...).
Assume + external fn
assume Name { external · guarantees } and external fn name(...) -> Bool { contract: ensures ... } surface trusted upstream contracts to the verifier.

LLM clients & routing

llm client declarations expose a typed tool surface. Tool schemas come from Carrier types; execution reapplies auth and policy context; transcripts persist in carrier_llm_conversations. 1.0 adds per-tenant USD budgets, downgrade behavior, and an explicit llm client routed wrapper for primary/fallback routing.

src/support/agents.carrier
llm client routed · budget · downgrade
llm client SupportAgent {
provider: "openai"
wire_format: "openai"
model: env("LLM_MODEL", "gpt-4.1-mini")
api_key: env("LLM_API_KEY", "")
max_tokens: 600
max_turns: 8
temperature: 0.2
budget_per_tenant_per_day_usd: 5.0
over_budget_behavior: downgrade("SupportAgentFallback")
tool search_help_docs(term: String) -> String[] = search_help_docs
}
llm client SupportAgentFallback {
provider: "openai"
wire_format: "openai"
model: "gpt-4.1-mini"
api_key: env("OPENAI_API_KEY", "")
max_tokens: 600
max_turns: 4
temperature: 0.2
}
llm client routed RoutedSupportAgent {
primary: SupportAgent
fallback: SupportAgentFallback
route_by: cost_vs_latency(target_per_request_usd: 0.05)
on_primary_outage: fallback
on_rate_limit: fallback
on_budget_pressure: fallback
}
Preferred call
Client.respond_as(TypeName, { user_prompt: ... }) obtains a typed structured response matching the Carrier type.
Budget enforcement
budget_per_tenant_per_day_usd clamps spend at call time using built-in pricing tables. Trace attributes: carrier.llm.budget.remaining_usd.
Routing & fallbacks
llm client routed takes a primary + one or more fallbacks with on_primary_outage, on_rate_limit, and on_budget_pressure escapes.

RAG declarations

Bind a vector retriever, an embedder, and an llm client into one typed declaration. The runtime trims serialized model context to the configured token budget and optionally reranks by score threshold.

src/support/rag.carrier
rag · retriever · llm
rag SupportAnswerer {
retriever: Doc.similar_with_scores
embed_with: embed_text
llm: SupportAgent
context_window_tokens: 4000
rerank: score_threshold(0.7)
top_k: 8
}
route POST "/support/answer" public -> String {
input: SimilarDocRequest
handler {
let response = SupportAnswerer.respond(input.query)
return response.text
}
}
Current rules
retriever binds to Model.similar_with_scores; the model must declare crud. embed_with must reference a fn or native fn with signature String -> Vector(N) matching the retriever model. Optional rerank: score_threshold(...).

Agents & chat threads

agent declarations are typed LLM agents with an input type, a system prompt, and a bound llm client. ui blocks can mount them as chat-thread participants whose inputs and durability are wired from the host chat session — one declaration, one mount path, one durability model.

src/ui/admin.carrier
ui · expose agent · thread_participant
ui AdminConsole {
framework: yew
theme: carrier.default
expose route "/docs" as list_detail_edit
expose agent SupportAgent as thread_participant {
identity: from_chat_session
thread: injected
response_channel: thread_reply
}
safety {
redact_pii: auto
require_auth: auto
tenant_scope: auto
}
}
Thread participants
expose agent SupportAgent as thread_participant requires identity: from_chat_session, thread: injected, and response_channel: thread_reply.
Conversation continuity
If the agent input declares conversation_id: String?, the generated manifest marks that field as the host thread/conversation binding so subsequent turns continue the same conversation.

Vector search

Fixed-dimension Vector(N) fields backed by pgvector. Carrier emits CREATE EXTENSION, the vector column DDL, HNSW/IVFFlat indexes, and generated similarity helpers when a model has exactly one vector field.

src/docs/vectors.carrier
Vector(1536) · hybrid_search
model Doc {
id: UUID
title: String
body: String
embedding: Vector(1536) @index(hnsw, metric: cosine)
created_at: Time
updated_at: Time
}
crud Doc at "/docs" {
list: public
get: public
searchable { title body }
}
native fn embed_text(value: String) -> Vector(1536) = "embed_text"
route POST "/docs/search" public -> DocHybridMatch[] {
input: SimilarDocRequest
handler {
return Doc.hybrid_search(
query: input.query,
embedding: embed_text(input.query),
limit: 20
)
}
}
Indexes
@index(hnsw, metric: cosine), ivfflat, and metric variants euclidean, inner_product.
Generated helpers
Model.similar_with_scores(...) returns <Model>SimilarityMatch[]; hybrid_search(...) returns <Model>HybridMatch[].
Embedding boundary
native fn embed_text(...) -> Vector(N) is the typed escape hatch; or accept precomputed vectors from an upstream.

Typed queues

Redis-backed queues with typed publish schemas, consumers bound to Carrier jobs, concurrency, ack modes, retry backoff, and dead-letter queues — declared in source.

src/queues/orders.carrier
queue · consume · dead_letter
type OrderPlacedPayload {
order_id: String
source: String
}
job enrich_order_event(payload: OrderPlacedPayload) -> Void {
logs.info("order received", { order_id: payload.order_id })
}
queue OrderEventDeadLetters {
backend: redis
subject: "orders.v1.dead"
publish OrderPlacedDead: OrderPlacedPayload
}
queue OrderEvents {
backend: redis
subject: "orders.v1"
publish OrderPlaced: OrderPlacedPayload
consume OrderPlaced by enrich_order_event {
concurrency: 2
ack: on_success
retry: backoff(delay_seconds: 15, max_attempts: 4)
dead_letter: OrderEventDeadLetters
}
}
Outbox pattern
Publish inside transaction { } so the message enqueue commits atomically with the DB write. Retry backoff is backoff(delay_seconds · max_attempts).

Tenant lifecycle

tenant declarations generate lifecycle hooks (on_create, on_suspend, on_delete) and durable purge windows via retain N days. Export format is declared in source.

src/tenants/organization.carrier
tenant · retain 30 days
model Organization {
org_id: String
name: String
suspended_at: Time?
deleted_at: Time?
}
tenant Organization {
id_field: org_id
export_format: ndjson
on_create {
audit.record("tenant.bootstrap", "Organization", tenant.org_id)
}
on_suspend {
logs.info("tenant suspended", { org_id: tenant.org_id })
}
on_delete {
purge_policy: retain 30 days
logs.warn("tenant scheduled for purge", { org_id: tenant.org_id })
}
}

Feature flags

Stateless deterministic evaluation with percentage rollouts and tenant-based targeting. Rules fire in declaration order; the first match wins.

src/main.carrier
flag · percentage · grouped_by
flag support_agent_v2 {
default: false
rules {
tenant in ["org_alpha", "org_beta"]: true
percentage(20) grouped_by tenant_id: true
}
}
route GET "/flags/support-agent-v2" protect Auth -> Json {
handler {
return { enabled: flags.enabled("support_agent_v2") }
}
}
Call surface
flags.enabled("flag_name") from routes, actions, workflows, or jobs. Flag declarations land in .carrier/manifest.json.

Realtime streams · watch

stream, subscription, and watch deliver typed events over transport-negotiated channels — WebSocket preferred, SSE fallback, long-poll last. watch compiles CRUD changes into typed events that fan out after the write transaction commits.

src/streams/doctors.carrier
stream · watch · tenant_field
event DoctorRealtimeEvent {
action: String
doctor_id: UUID
}
stream DoctorEvents
at "/streams/doctors"
protect AdminAuth roles [admin]
-> DoctorRealtimeEvent
{
tenant_field: tenant_id
workspace_field: workspace_id
group_field: doctor_id
}
watch Doctor {
on create emit DoctorRealtimeEvent { action: "created", doctor_id: value.id }
on update emit DoctorRealtimeEvent { action: "updated", doctor_id: value.id }
on delete emit DoctorRealtimeEvent { action: "archived", doctor_id: old.id }
on restore emit DoctorRealtimeEvent { action: "restored", doctor_id: value.id }
}
Endpoints
GET /.../negotiate · ws · sse · poll. cursor / group / wait_ms query parameters supported.
Scoping
tenant_field · workspace_field · group_field select the payload columns used for channel-scoped delivery.

Native OTLP telemetry

service.telemetry enables OpenTelemetry export for traces, metrics, and logs. The runtime emits zero-boilerplate spans for every execution boundary. trace.annotate and metrics.counter/gauge cover user-authored observability.

src/main.carrier
opentelemetry · otlp_http
service App {
openapi { title: "App API" version: "0.9.0" }
server { host: env("HOST", "0.0.0.0") port: env_int("PORT", 3000) }
telemetry {
provider: opentelemetry
endpoint: env("OTEL_EXPORTER_OTLP_ENDPOINT", "")
service_name: "app-api"
protocol: otlp_http
sampling: parentbased_ratio(0.1)
}
}
route GET "/checkout" protect Auth -> CheckoutResult {
handler {
let actor = auth.current_user()
trace.annotate({ actor_id: actor.id, tenant_id: actor.tenant_id })
metrics.counter("checkout.requests").increment({ tenant_id: actor.tenant_id })
return Checkout.place(input)
}
}
Sampling
always, never, ratio(...), parentbased_ratio(...) — service-level or route-level override.
Stable attributes
carrier.route_name · workflow_name · step_name · tenant_id · request_id · outcome · error_code.
User-authored
trace.annotate({ ... }), metrics.counter(...).increment(...), metrics.gauge(...).record(...).

In-language tests

test blocks declare HTTP-level scenarios with optional auth context, a given/when/then rhythm, and typed response helpers. carrier test runs the whole suite against the generated service.

src/main.carrier
test · given · when · then
test "me endpoint uses authenticated scenario user" {
auth {
id: 7
email: "viewer@example.com"
name: "Viewer"
roles: ["user"]
}
given {
let health = http.request(method: "GET", path: "/health")
}
when {
let me = http.request_as("MeResponse", method: "GET", path: "/me")
}
then health.status == 200 && me.email == "viewer@example.com"
}

UI & api_explorer

ui declarations mount compiler-owned shells (Yew today) whose generated specs stay aligned with Carrier routes, CRUD, auth, and PII redaction. api_explorer adds a typed Try-it surface with named environments and replayable collection checks against the same manifest.

src/ui/admin.carrier
ui · expose route · expose agent
ui AdminConsole {
framework: yew
theme: carrier.default
expose route "/docs" as list_detail_edit
expose agent SupportAgent as thread_participant {
identity: from_chat_session
thread: injected
response_channel: thread_reply
}
safety {
redact_pii: auto
require_auth: auto
tenant_scope: auto
}
}
src/explorers/hospital.carrier
api_explorer · collection · capture
api_explorer HospitalApi {
upstream: service
mount_target: full_panel
collection AdmissionFlow {
step admit {
call: action admit_patient
params: { input: { mrn: "TEST001", age: 65 } }
capture {
admission_id: response.id
admitted_at: response.admitted_at
}
}
step record_vitals {
call: route POST "/vitals/{admission_id}"
params: { admission_id: admission_id }
input: { admission_id: admission_id, bp: "130/85", hr: 72 }
assert: status == 200 && response.recorded_at > admitted_at
}
}
safety {
redact_pii: auto
audit_every_request: true
max_environment: dev_or_staging
}
}
Mount paths
Default mount path is /ui/{snake_case(name)} with spec.json and app.wasm companions. Carrier emits mount_protocol: carrier.chat.web_component.
Federated upstreams
api_explorer Foo { upstream: federated BarService } browses imported manifest actions/workflows. Use environments { ... } to declare named base URLs, auth, and deterministic / mock flags.
Replay safety
Collections type-check params · query · input against exposed targets. Capture/assert expressions are deterministic — no side-effect calls allowed.

Evals & finetune

eval declarations turn dataset rows into graded LLM regression checks that run under carrier test. finetune builds curated training datasets from persisted llm client conversations and pins promoted models explicitly.

src/evals/summaries.carrier
eval · dataset · grade · require
type SummaryFixture {
prompt: String
expected: String
}
eval "summaries stay factual" {
dataset: "./evals/summary_fixtures.jsonl"
forall sample: SummaryFixture
when {
let actual = SupportAgent.respond_as(String, {
user_prompt: sample.prompt
})
}
grade actual == sample.expected
require pass_rate >= 0.9
}
Aggregate gates
require takes any Bool built from pass_rate, total_cases, passed_cases, and failed_cases.
Prompt optimization
carrier eval optimize <eval> --llm-client <c> reruns the same eval against bounded system_prompt variants and writes a recommendation report under .carrier/prompt-optimizations/.

PDF rendering

The runtime ships two complementary PDF helpers: pdf.render_html for deterministic text-first output, and pdf.render_svg_template for rich visual output with custom fonts and embedded assets. Both compose with blob.put + blob.signed_url for distribution.

src/certificates.carrier
pdf.render_svg_template · blob
type CertificateContext {
recipient_name: String
completion_date: String
instructor_name: String
}
route POST "/certificates/{recipient_name}" public -> String {
handler {
let ctx = CertificateContext {
recipient_name: params.recipient_name
completion_date: "April 23, 2026"
instructor_name: "Niko Ma"
}
let pdf = pdf.render_svg_template(
load_template("ryt200.svg"),
ctx,
"/tmp/certs/" + ctx.recipient_name + ".pdf",
font_paths: ["/fonts/PlayfairDisplay.ttf"],
resource_dir: "/tmp/cert-assets",
title: "RYT-200 Certificate"
)
let stored = blob.put(pdf.path, "certs/" + ctx.recipient_name + ".pdf", pdf.content_type)
return blob.signed_url(stored.key, expires_seconds: 300, download_name: pdf.filename)
}
}

External clients

Typed outbound JSON. Base URL, per-request headers, timeout, and a typed response mapping.

api/src/main.carrier
WeatherApi
client WeatherApi {
base_url: env("WEATHER_API_URL", "https://api.example.com")
header "x-api-key": env("WEATHER_API_KEY", "")
timeout_ms: 3000
}
type WeatherResponse {
city: String
temperature_c: Float
condition: String
}
route GET "/weather/{city}" public -> WeatherResponse {
handler {
return WeatherApi.get("/forecast", query: { city: params.city })
}
}

SQL & DB escape hatches

When model helpers aren't expressive enough — aggregates, joins, reports, or stored functions — sql.* and db.* map the result into a typed shape.

api/src/main.carrier
sql.list_as
/// Admin reporting endpoint using raw SQL mapped into a typed result list
route GET "/reports/doctors/by-city" protect AdminAuth roles [admin]
-> DoctorCityReportRow[]
{
handler {
return sql.list_as(
"DoctorCityReportRow",
"select city, count(*)::bigint as total from doctors where deleted_at is null group by city order by total desc, city asc"
)
}
}

Built-in namespaces

A compact set of runtime-bound namespaces covers the day-to-day backend surface. Each is typed, documented, and implemented today.

auth
  • auth.register(...)
  • auth.login(...)
  • auth.issue_tokens(...)
  • auth.current_user()
logs
  • logs.debug(...)
  • logs.info(...)
  • logs.warn(...)
  • logs.error(...)
cache · redis
  • cache.get_as(type_name, key)
  • cache.set(key, value, ttl_seconds: 120)
  • cache.exists(key)
  • redis.publish(channel, message)
jobs · audit
  • jobs.enqueue(name, payload, delay_seconds: 5)
  • jobs.enqueue(name, payload, baggage: { ... })
  • audit.record(action, entity, entity_id, metadata)
sql · db
  • sql.list_as(type_name, sql, ...)
  • sql.one_as(type_name, sql, ...)
  • db.fn_scalar_as(type_name, fn, ...)
flags · workflows
  • flags.enabled("flag_name")
  • WorkflowName.start(input, baggage: { ... })
  • WorkflowName.status(run_id)
  • WorkflowName.result(run_id)
  • WorkflowName.retry_compensation(run_id)
trace · metrics
  • trace.annotate({ key: value })
  • metrics.counter("name").increment({ ... })
  • metrics.gauge("name").record(value, { ... })
http (tests)
  • http.request(method, path)
  • http.request_as("TypeName", method, path)
string · array · json
  • string.lower · trim · slug · contains
  • array.length · push · contains
  • json.parse · stringify · get

Current limits

Carrier is explicit about what it does not do yet. Everything here is intentional and documented.

Imports
Imports are validated metadata. They are not yet selective loaders — every .carrier file under src/ still compiles together.
Migrations
Structural diffing only. Renames are not inferred automatically.
Transactions
return inside transaction { } is not supported yet.
Scheduled jobs
Scheduled jobs currently enqueue with an empty JSON payload. Prefer Json payload types.