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"

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.

The kind string becomes the prefix of generated Id values for this record: a record with kind "post" produces IDs like post_01HQT2....

Checker errorCause
E0112Root record missing required kind declaration
E0113kind used on a non-root (child) record
E0114kind 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
}

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"
}

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