Concepts Overview

The mental model behind Grove: projects, modules, the action-event-record pipeline, and how workflows fit in. Understanding these concepts makes every other page in this documentation click.

Prerequisites: Hello World or 5-Minute Tutorial. What you'll learn: How Grove's core abstractions relate to each other.

The Hierarchy

Project (grove.toml)
  +-- Module (directory)
        +-- Record -- the entity being modeled
        +-- Events -- immutable facts
        +-- Actions -- entry points for change
        +-- Validators -- reusable rules
        +-- Types & Enums -- data shapes
        +-- Tests -- built-in test runner

Project

A project is a repository with a grove.toml at the root. The project_id becomes the first segment of all resource identifiers.

project_id = "acme"

Module

A directory containing .grove files. All files in the directory are merged into one scope -- you can split declarations across files for organization. The directory name is the module name.

acme/
  grove.toml            # project_id = "acme"
  orders/
    module.grove         # record, events, actions
    types.grove          # type and enum declarations
    orders_test.grove    # tests (excluded from compilation)
  shipping/
    module.grove

Area 1: Aggregates

The aggregate pattern is the core of Grove. Every state change follows this pipeline:

Action → validate → emit Event(s) → apply to Record

Record

The current state of an entity. One record per module. Fields have types, optional markers, and default values.

record {
  kind "order"
  title: String
  status: String = "draft"
  items: List<OrderItem>
  total: Decimal?
}

Records also contain invariants (conditions that must always hold) and state machines (valid field transitions).

Event

An immutable fact describing something that happened. Events are stored permanently and can be replayed to reconstruct any past state.

event order_placed {
  customer_id: Id
  items: List<OrderItem>
  total: Decimal
}

Action

The only way to change state. Actions receive input (params), validate it, and emit events in their apply block.

action place_order {
  customer_id: Id
  items: List<OrderItem>
  validate has_items {
    params.items.len > 0
  }
  apply {
    let total = params.items.map(|i| i.price * i.quantity).sum()
    emit order_placed {
      customer_id: params.customer_id
      items: params.items
      total: total
    }
  }
}

The Flow

  1. A request arrives with an action name, parameters, and metadata
  2. Validation runs -- both reusable validators and inline validate blocks
  3. The apply block executes, emitting one or more events
  4. Each event is stored and applied to the record
  5. Invariants are checked against the new record state
  6. State machine transitions are verified
  7. The new record state is returned

If any step fails -- validation, invariant violation, invalid state transition -- the entire operation is rejected. No events are stored, no state changes.

Area 2: Workflows

Workflows extend Grove beyond single-entity operations into multi-step, multi-service orchestration.

Workflow

A directed acyclic graph (DAG) of activities. Nodes are units of work; edges define execution order.

workflow fulfill_order v1 {
  node validate = validate_inventory v1
  node charge = charge_payment v1
  node ship = create_shipment v1

  edge validate -> charge
  edge charge -> ship
}

Activity

A unit of work with input, data operations, and output. Activities can call external services, publish messages, invoke other modules, or query databases.

activity charge_payment v1 {
  data charge {
    fetch stripe.create_charge {
      amount: input.amount
      currency: input.currency
    }
  }
  apply(input: ChargeInput) -> ChargeResult {
    return { charge_id: charge.id, status: charge.status }
  }
}

Service

External API contracts. Services declare operations with their inputs, outputs, and authentication.

Trigger

Starts a workflow when an event occurs in a module.

Schedule

Starts a workflow on a cron schedule.

Type System

Grove uses a nominal type system -- types with the same fields but different names are not interchangeable.

CategoryTypes
ScalarsInt, Decimal, Bool, String, Id, Date, Time, DateTime, Duration, Epoch
CompositesList<T>, Map<K, V>, T? (optional)
Customtype Name { ... }, enum Name { ... }

Root Objects

Expressions in Grove always start from a root object that depends on context:

ContextAvailable roots
Action applyparams, record, meta
Event applyevent, record, meta
Validateparams, record
Invariantrecord
Activity applyinput, data block names
Test assertionrecord, events

Resource Naming

Every construct has a global name with four parts: project::module::name::version.

acme::orders::place_order::v1

In .grove source files, you use . separators and space-separated versions:

trigger on_placed from orders.order_placed { ... }
node charge = charge_payment v1

The compiler maps these to their full resource names internally.

What's Next

TopicPage
Build something realBuild a Task Tracker
Learn validation deeplyValidation & State Machines
Explore types and expressionsExpressions Deep Dive
Run against a databaseDatabase Backends
See the full grammarDeclaration Grammar

Authentication and Authorization

Two more concepts round out the mental model, each with its own section in these docs:

You can build a complete Grove project without either — Manzano will run without authentication configured, and actions without authorize hooks are permitted by default.

See Also