Declaration Grammar
Grove programs are composed of top-level declarations that define the shape of data, the events that drive state changes, the actions that handle commands, and the workflows that orchestrate long-running processes. This page provides the complete EBNF grammar for every declaration form.
Prerequisites: Lexical Grammar for token definitions. What you'll learn: The syntactic structure of all top-level declarations in Grove.
Overview
A Grove source file is a sequence of top-level declarations. The order of declarations within a file does not matter; all declarations in a module are merged before analysis.
SourceFile = { Declaration } .
Declaration = ImportDecl
| TypeDecl
| EnumDecl
| RecordDecl
| EventDecl
| ActionDecl
| ValidateDecl
| UpcastDecl
| FunctionDecl
| WorkflowDecl
| ActivityDecl
| ServiceDecl
| RouteDecl
| QueryDecl
| TriggerDecl
| ScheduleDecl
| WebhookDecl
| TestDecl .
Import Declaration
ImportDecl = "import" ImportPath [ "as" Identifier ] .
ImportPath = StringLiteral .
Imports bring names from other modules into scope. See Module Resolution for path semantics.
import "grove/time"
import "grove/http" as http
import "../shared/types"
- Paths prefixed with
grove/resolve to the standard library. - All other paths are resolved relative to the importing file's directory.
- The optional
asclause provides an alias for the imported module.
Type Declaration
TypeDecl = "type" TypeName "=" TypeExpr .
TypeName = Identifier .
Type declarations create named type aliases.
type Money = Decimal
type UserId = Id
type Metadata = Map<String, Value>
Enum Declaration
EnumDecl = "enum" EnumName "{" EnumVariant { "," EnumVariant } [ "," ] "}" .
EnumName = Identifier .
EnumVariant = Identifier .
Enums define a closed set of named variants. Variants are referenced as EnumName.Variant.
enum OrderStatus {
Pending,
Confirmed,
Shipped,
Delivered,
Cancelled,
}
Record Declaration
RecordDecl = "record" [ Identifier ] [ "@parent" "(" Identifier ")" ] "{" RecordBody "}" .
RecordBody = KindDecl? FieldDecl* ( StateMachineDecl | InvariantDecl )* .
KindDecl = "kind" StringLiteral .
FieldDecl = { Annotation } FieldName ":" TypeExpr [ "=" DefaultExpr ] .
FieldName = Identifier .
Annotation = "@pii" | "@confidential" | "@lookup" "(" Identifier ")" .
Every module has at most one root record — the aggregate the module models. The root record is unnamed and declares a kind string that becomes the prefix of every generated ID for that aggregate.
record {
kind "post"
title: String
body: String
@pii author_email: String
}
The kind declaration
The kind declaration is required on every root record and must be the first item inside the record body.
- 2-5 ASCII lowercase letters (
a-z). - Must not start with
mz(reserved for system-generated IDs). - Not a reserved keyword.
- Must be unique within a project.
The kind string becomes the prefix of generated Id values for this record: a record with kind "post" produces IDs like post_01HQT2....
| Checker error | Cause |
|---|---|
E0112 | Root record missing required kind declaration |
E0113 | kind used on a non-root (child) record |
E0114 | kind value is invalid (too long, uppercase, mz prefix, or reserved word) |
Child records
A named record with @parent(...) declares a nested child record that shares the parent's aggregate boundary. Child records do not declare their own kind; they inherit identity from their parent.
record Comment @parent(post) {
author: String
body: String
posted_at: DateTime
}
See Module Resolution for how records, events, and actions within a module relate.
Event Declaration
EventDecl = [ EventModifier ] "event" EventName [ ":" Version ] "{" { FieldDecl } "}" .
EventModifier = "@create" | "@delete" .
EventName = Identifier .
Version = IntLiteral .
Events represent facts that have occurred. They are the fundamental unit of state change in Grove applications.
@create
event OrderCreated:1 {
customer_id: Id
items: List<OrderItem>
total: Decimal
}
event OrderShipped:1 {
tracking_number: String
carrier: String
}
@delete
event OrderArchived:1 {
reason: String
}
@createmarks the event that initializes an entity. Exactly one@createevent is required per module that defines events.@deletemarks the event that logically deletes an entity.- The version number (
:1) is required and must be a positive integer.
Action Declaration
ActionDecl = "action" ActionName "(" ParamList ")" [ "->" TypeExpr ] "{" { Statement } "}" .
ActionName = Identifier .
ParamList = [ Param { "," Param } ] .
Param = ParamName ":" TypeExpr .
ParamName = Identifier .
Actions are the command handlers of a Grove application. They validate input, apply business logic, and emit events.
action create_order(customer_id: Id, items: List<OrderItem>) -> OrderId {
let order_id = Id.generate()
emit OrderCreated {
customer_id: customer_id
items: items
total: items.map(i => i.price * i.quantity).sum()
}
return order_id
}
Validate Declaration
ValidateDecl = "validate" EventName "{" { Statement } "}" .
Validate blocks define invariant checks that run before an event is persisted. The body must consist exclusively of fail statements (typically guarded by if conditions).
validate OrderCreated {
if items.is_empty() {
fail "Order must contain at least one item"
}
if total <= 0 {
fail "Order total must be positive"
}
}
Upcast Declaration
UpcastDecl = "upcast" EventName ":" FromVersion "->" ToVersion "{" { Statement } "}" .
FromVersion = IntLiteral .
ToVersion = IntLiteral .
Upcasts define transformations from older event versions to newer ones, enabling schema evolution without data migration.
upcast OrderCreated:1 -> 2 {
// v1 had no currency field; default to USD
let currency = "USD"
}
ToVersionmust equalFromVersion + 1.- Upcasts form a chain: if versions 1, 2, and 3 exist, upcasts
1->2and2->3are required.
Function Declaration
FunctionDecl = "function" FunctionName "(" ParamList ")" "->" TypeExpr "{" { Statement } "}" .
FunctionName = Identifier .
Functions are pure, side-effect-free computations. They cannot emit events or perform I/O.
function calculate_tax(subtotal: Decimal, rate: Decimal) -> Decimal {
return subtotal * rate
}
Workflow Declaration
WorkflowDecl = "workflow" WorkflowName "(" ParamList ")" [ "->" TypeExpr ] "{" { Statement } "}" .
WorkflowName = Identifier .
Workflows define durable, long-running processes that survive restarts. They may call activities and other workflows.
workflow process_order(order_id: Id) {
let payment = charge_payment(order_id)
if payment.success {
emit OrderConfirmed { order_id: order_id }
ship_order(order_id)
} else {
emit OrderCancelled { order_id: order_id, reason: "Payment failed" }
}
}
Activity Declaration
ActivityDecl = "activity" ActivityName "(" ParamList ")" [ "->" TypeExpr ] "{" { Statement } "}" .
ActivityName = Identifier .
Activities are the units of external interaction within workflows. Each activity execution is individually retried on failure and its result is durably recorded.
activity charge_payment(order_id: Id) -> PaymentResult {
let order = query_order(order_id)
return payment_service.charge(order.total, order.currency)
}
Service Declaration
ServiceDecl = "service" ServiceName "{" { MethodDecl } "}" .
ServiceName = Identifier .
MethodDecl = Identifier "(" ParamList ")" [ "->" TypeExpr ] .
Services declare external API interfaces that activities can call. The runtime binds service methods to actual HTTP endpoints or SDK calls.
service payment_service {
charge(amount: Decimal, currency: String) -> PaymentResult
refund(charge_id: Id) -> RefundResult
}
Route Declaration
RouteDecl = "route" HttpMethod StringLiteral "=>" ActionRef .
HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" .
ActionRef = [ ModuleName ":" ] ActionName .
Routes map HTTP endpoints to actions.
route POST "/orders" => create_order
route GET "/orders/:id" => get_order
route DELETE "/orders/:id" => cancel_order
Query Declaration
QueryDecl = "query" QueryName "(" ParamList ")" "->" TypeExpr "{" { Statement } "}" .
QueryName = Identifier .
Queries define read-only operations against projected state. They cannot emit events.
query get_order(order_id: Id) -> Order {
return orders.get(order_id)
}
Trigger Declaration
TriggerDecl = "trigger" EventName "=>" HandlerRef .
HandlerRef = Identifier .
Triggers connect events to workflows or actions that should execute in response.
trigger OrderCreated => process_order
trigger PaymentFailed => notify_customer
Schedule Declaration
ScheduleDecl = "schedule" CronExpr "=>" HandlerRef .
CronExpr = StringLiteral .
Schedules define time-based triggers using cron expressions.
schedule "0 0 * * *" => daily_reconciliation
schedule "*/5 * * * *" => check_pending_orders
Webhook Declaration
WebhookDecl = "webhook" WebhookName "{" { WebhookField } "}" .
WebhookName = Identifier .
WebhookField = Identifier ":" Expression .
Webhooks define inbound webhook endpoints with signature verification and payload mapping.
webhook stripe_events {
provider: "stripe"
secret: credentials.stripe_webhook_secret
path: "/webhooks/stripe"
handler: handle_stripe_event
}
Test Declaration
TestDecl = "test" StringLiteral "{" { Statement } "}" .
Tests define executable test cases. They appear in *_test.grove files and are excluded from production builds.
test "creating an order emits OrderCreated" {
let result = create_order(customer_id: Id.of("cust-1"), items: [
OrderItem { product_id: Id.of("prod-1"), quantity: 2, price: 10.00 }
])
assert result != null
}
See Also
- Lexical Grammar -- token-level syntax
- Expression Grammar -- expressions used within declaration bodies
- Statement Grammar -- statements used within declaration bodies
- Semantic Constraints -- validation rules applied after parsing
- Module Resolution -- how declarations across files are merged