- Docs
- Reference
- .zen syntax
.zen syntax
Reference for the Zever schema language: entities, relations, services, RPCs, jobs, schedules, sagas, types, and attributes.
.zen files describe services once, in a proto-inspired syntax. The compiler turns the schema into database access, routing, and queue plumbing, never business logic. Format with zever fmt, validate with zever check, generate with zever compile (see the CLI reference).
Files and modules
Section titled “Files and modules”The module name comes from the immediate subdirectory, not the file name: schema/app/app.zen is module app. A directory such as schema/v1/iam/ also gives the module a version (v1), and a warning flags RPC paths not starting with /v1/.
Types in the same module reference each other by bare name, with no import. Cross-module references use the module path, and zever check-boundaries reports relations, RPC return types, and permission resources that cross modules. Sagas may reference RPCs across modules by design.
Declarations
Section titled “Declarations”| Declaration | Form | Purpose |
|---|---|---|
entity |
entity Name { fields, relations, indexes } |
Persisted record. |
service |
service Name { rpc ... } |
Group of RPCs. |
rpc |
rpc Name(params) -> Return { options } |
Operation with transport bindings. |
enum |
enum Name { a, b } |
Named closed set of values. |
message |
message Name { fields } |
Non-persisted payload shape. |
job |
job Name(params) { queue: q retry: ... } |
Background job. |
schedule |
schedule Name { cron: "..." dispatch: Job() } |
Cron trigger for a job. |
saga |
saga Name { step s { execute: Svc.Rpc } } |
Compensatable RPC sequence. |
Reserved words: entity, service, job, schedule, rpc, index, enum, has_many, has_one, belongs_to, many_to_many, message, true, false. Type names, option labels (http, auth, queue, …), HTTP verbs, saga, step, and join_table are contextual, not reserved. Comments start with //, and a comment directly above a declaration becomes its doc comment.
| Type | Notes |
|---|---|
uuid, string, bool, bytes, json |
Scalars. |
int32, int64, float32, float64 |
Numeric scalars. |
timestamp, date |
Time scalars. datetime is an alias of timestamp. |
enum(a, b) |
Inline enum with unique values. Use a named enum to share one. |
Name |
Reference to an entity or message. |
Append ? to make a field optional: bio: string?.
Enums and messages
Section titled “Enums and messages”enum Status { active, inactive }
message LoginRequest { email: string @validate(format: "email") remember: bool}
entity Account { id: uuid @primary status: Status @default(active)}A named enum needs at least one value and unique values, and its name cannot collide with an entity. A message carries fields but is never persisted.
Entities
Section titled “Entities”entity User { id: uuid @primary email: string @unique @validate(format: "email") created_at: timestamp @default(now())
has_many orders: Order @foreign_key(user_id) index(email, created_at)}
entity Order { id: uuid @primary user_id: uuid amount_cents: int64
belongs_to user: User @foreign_key(user_id)}Declare both sides of a relation with the same @foreign_key.
Field attributes
Section titled “Field attributes”| Attribute | Applies to | Meaning |
|---|---|---|
@primary |
field | Primary key. |
@unique |
field | Unique constraint. |
@default(x) |
field, message field | Default. now() only on timestamp; an enum default names a declared value; a literal must match the scalar family. @default(true) and @default(false) do not parse today because true and false are reserved words. |
@validate(...) |
field, RPC param, message field | Validation rules (below). |
@renamed_from("old") |
field | Previous column name, so zever db migrate emits a rename not a drop and add. The old name must not still be declared. |
Validation rules
Section titled “Validation rules”| Rule | Applies to | Value |
|---|---|---|
format |
string |
"email", "url", or "uuid". |
min_len, max_len |
string |
Integer length. |
gt, gte, lt, lte |
numeric scalars | Numeric literal. |
Combine rules: @validate(min_len: 1, max_len: 100). @validate is rejected on a field or param whose type is an entity or message; put it on that type’s fields instead.
Every field of an entity or message used as an RPC parameter must carry at least one applicable @validate rule. Return-only types are exempt, as are fields with no applicable rule (uuid, bool, timestamp, date, bytes, json, enums).
Entity attributes
Section titled “Entity attributes”| Attribute | Meaning |
|---|---|
@schema(name) |
Database schema. Defaults to the module name, or public when unnamed. |
Relations
Section titled “Relations”| Keyword | Form | Notes |
|---|---|---|
has_many |
has_many orders: Order @foreign_key(user_id) |
One to many. |
has_one |
has_one profile: Profile @foreign_key(user_id) |
One to one. |
belongs_to |
belongs_to user: User @foreign_key(user_id) |
Owning side. |
many_to_many |
many_to_many tags: Tag { join_table: post_tags } |
No @foreign_key. Names the join table. |
| Relation attribute | Values |
|---|---|
@foreign_key(field) |
Required on every relation except many_to_many. |
@on_delete(x) |
restrict, cascade, or set_null. |
Indexes
Section titled “Indexes”index(a, b) declares a secondary index over one or more fields. Index attributes follow the closing parenthesis.
Grammar summary
Section titled “Grammar summary”file = { entity | service | enum | message | job | schedule | saga }entity = "entity" Name { attr } "{" { field | relation | index } "}"field = name ":" type [ "?" ] { attr }relation = ("has_many" | "has_one" | "belongs_to" | "many_to_many") name ":" Name { attr } [ "{" "join_table" ":" name "}" ]index = "index" "(" name { "," name } ")" { attr }service = "service" Name "{" { rpc } "}"rpc = "rpc" Name "(" [ param { "," param } ] ")" "->" Type "{" { option } "}"param = name ":" type { attr }attr = "@" name [ "(" args ")" ]option = label ":" valueServices and RPCs
Section titled “Services and RPCs”service UserService { rpc GetUser(id: uuid) -> User { http: GET "/v1/users/{id}" auth: required }
rpc CreateUser(email: string @validate(format: "email")) -> User { http: POST "/v1/users" auth: required errors: { already_exists } }
rpc ListOrders(user_id: uuid) -> Order { http: GET "/v1/orders" auth: required paginated: true }}| Option | Value | Effect |
|---|---|---|
http: |
GET, POST, PUT, PATCH, DELETE, then "/path/{param}" |
HTTP binding. Every {param} needs a same-named RPC input. |
auth: |
required or none |
Authentication. Every RPC must declare auth: or permission:; an RPC with neither fails to resolve (secure by default), so write auth: none for a public route. Generated gRPC servers are fail-closed: auth: none operations are listed with an explicit empty policy, and any unlisted method is denied. |
permission: |
check("action", resource: Entity, owner_field: field) |
Authorization. resource must be an entity, owner_field one of its fields. |
errors: |
{ code, code("message") } |
Declared error codes (table below). Duplicates are rejected. |
paginated: |
true or false |
true adds cursor/limit request fields and a <Op>Response page (items, next_cursor) to one RPC, on both HTTP and gRPC. |
Every RPC gets a gRPC transport. Repeating http:, auth:, permission:, or paginated: silently keeps the last value.
Error codes
Section titled “Error codes”| Code | HTTP |
|---|---|
cancelled |
499 |
unknown, internal, data_loss |
500 |
invalid_argument, failed_precondition, out_of_range |
400 |
deadline_exceeded |
504 |
not_found |
404 |
already_exists, aborted |
409 |
permission_denied |
403 |
unauthenticated |
401 |
resource_exhausted |
429 |
unimplemented |
501 |
unavailable |
503 |
Jobs, schedules, and sagas
Section titled “Jobs, schedules, and sagas”job SendWelcomeEmail(user_id: string) { queue: default retry: max_attempts(3), backoff(exponential, base: 30s)}
schedule DailyReport { cron: "0 9 * * *" dispatch: GenerateDailyReport()}
saga PlaceOrder { step reserve { execute: StockService.Reserve compensate: StockService.Release } step charge { execute: PaymentService.Charge pivot: true }}| Option | Block | Value |
|---|---|---|
queue: |
job | Queue name. |
retry: |
job | max_attempts(n) with n > 0, and backoff(exponential, base: <duration>). Exponential is the only backoff kind. |
cron: |
schedule | Cron expression. |
dispatch: |
schedule | JobName(args) naming a declared job. |
execute: |
saga step | Service.Rpc. Required. |
compensate: |
saga step | Service.Rpc that undoes the step when a later step fails. |
pivot: |
saga step | true rolls failures after this step forward instead of compensating. At most one per saga. |
Step names are unique within a saga. Durations use literals such as 30s.
Diagnostics
Section titled “Diagnostics”The parser never panics. Malformed input yields best-effort output plus positioned diagnostics, and the formatter refuses input that fails lexing.
| Symptom | Cause |
|---|---|
| Unknown scalar type | Spelling is not in the type table. The compiler suggests the closest scalar. |
| Unresolved relation target | Target missing, in another module without a module path, or the two sides name different @foreign_key fields. |
| HTTP param has no RPC input | A {param} in the http: path has no same-named input. |
| Unknown attribute | Entity allows only @schema. Fields allow @primary, @unique, @validate, @default, @renamed_from. RPC params and message fields allow @validate (message fields also @default). |
Related
Section titled “Related”- Resolver and IR for the compile pipeline and diagnostics.
- Backends for generated outputs and golden files.
- Editor setup for editor support and
make generate.