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.
Top-level declarations
Thirty-three declarations. Every Carrier program is composed from this set. No imports bring in new grammar; no macros hide behavior.
Types & annotations
Carrier has named records, enums, arrays, optionals, and a tight scalar set. Validation and DB metadata attach with @-annotations.
StringUTF-8 stringInt · Floatchecked i64 / 64-bit floating pointBoolbooleanUUIDUUIDTime · Datetimestamp · calendar dateJson · Voidarbitrary JSON · no valueVector(N)pgvector-backed embeddingT[]array of TT?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
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[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" }[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"carrier install resolves the highest non-revoked semver version, verifies signature + provenance + SBOM, and writes everything to carrier.lock.carrier audit --format json --fail-on high checks registry advisories + OSV.dev. CI-friendly machine output.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.
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.
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) }}admin includes [editor, viewer] grants admin the implicit editor and viewer capabilities for role checks.protect Auth roles [a, b] is any-of. Use protect Auth all_roles [a, b] when both must be present.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.
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 }}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/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.
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 }}exact, contains, and min today.active. Internal callers can pass all or deleted.Routes
Explicit HTTP. Typed path params, query blocks, input bodies, response types, auth forms, and optional idempotency.
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) }}public, protect Auth, protect Auth roles [admin, editor].input, query { }, and params { }.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.
/// Transaction-safe admin action with row lockingaction 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 responseroute POST "/admin/doctors/{id}/lock" protect AdminAuth roles [admin] idempotent -> Doctor{ params { id: UUID } handler { return audit_locked_doctor(params.id) }}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.
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}ENABLE ROW LEVEL SECURITY + FORCE ROW LEVEL SECURITY.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.
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.
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}carrier_workflow_state; graph step tasks in carrier_workflow_tasks.WorkflowStatus: queued · running · retry · completed · failed · compensating · compensated · compensation_failed · timed_out.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.
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}event("..."), status("..."), step_started("Step"), step_completed("Step").attainment: 95%, deadline: 4_hours, measure_by: calendar_month | rolling_7_days.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.
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 { solver: z3, mode: bounded, max_records, max_workflow_depth, timeout }.must_always, must_never, transition-style transition: field -> value { requires }, bounded existentials with exists Model where ... and overlaps(...).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.
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}Client.respond_as(TypeName, { user_prompt: ... }) obtains a typed structured response matching the Carrier type.budget_per_tenant_per_day_usd clamps spend at call time using built-in pricing tables. Trace attributes: carrier.llm.budget.remaining_usd.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.
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 }}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.
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 }}expose agent SupportAgent as thread_participant requires identity: from_chat_session, thread: injected, and response_channel: thread_reply.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.
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 ) }}@index(hnsw, metric: cosine), ivfflat, and metric variants euclidean, inner_product.Model.similar_with_scores(...) returns <Model>SimilarityMatch[]; hybrid_search(...) returns <Model>HybridMatch[].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.
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 }}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.
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.
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") } }}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.
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 }}GET /.../negotiate · ws · sse · poll. cursor / group / wait_ms query parameters supported.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.
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) }}always, never, ratio(...), parentbased_ratio(...) — service-level or route-level override.carrier.route_name · workflow_name · step_name · tenant_id · request_id · outcome · error_code.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.
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.
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 }}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 }}/ui/{snake_case(name)} with spec.json and app.wasm companions. Carrier emits mount_protocol: carrier.chat.web_component.api_explorer Foo { upstream: federated BarService } browses imported manifest actions/workflows. Use environments { ... } to declare named base URLs, auth, and deterministic / mock flags.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.
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}require takes any Bool built from pass_rate, total_cases, passed_cases, and failed_cases.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.
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.
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.
/// Admin reporting endpoint using raw SQL mapped into a typed result listroute 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.register(...)
- auth.login(...)
- auth.issue_tokens(...)
- auth.current_user()
- logs.debug(...)
- logs.info(...)
- logs.warn(...)
- logs.error(...)
- cache.get_as(type_name, key)
- cache.set(key, value, ttl_seconds: 120)
- cache.exists(key)
- redis.publish(channel, message)
- jobs.enqueue(name, payload, delay_seconds: 5)
- jobs.enqueue(name, payload, baggage: { ... })
- audit.record(action, entity, entity_id, metadata)
- sql.list_as(type_name, sql, ...)
- sql.one_as(type_name, sql, ...)
- db.fn_scalar_as(type_name, fn, ...)
- flags.enabled("flag_name")
- WorkflowName.start(input, baggage: { ... })
- WorkflowName.status(run_id)
- WorkflowName.result(run_id)
- WorkflowName.retry_compensation(run_id)
- trace.annotate({ key: value })
- metrics.counter("name").increment({ ... })
- metrics.gauge("name").record(value, { ... })
- http.request(method, path)
- http.request_as("TypeName", method, path)
- 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.
.carrier file under src/ still compiles together.return inside transaction { } is not supported yet.Json payload types.