LLM Cheat Sheet

Fast, parser-accurate generation guide for Grove source. This page is optimized for LLM consumption -- minimal prose, maximum signal.

Purpose: Enable reliable first-pass code generation from LLM agents.

1. Minimal Mental Model

Grove modules are made of top-level declarations. A directory is a module. All .grove files in the directory merge into one scope.

Top-level declarations: import, type, enum, record, event, action, validate, upcast, function, trigger, schedule, workflow, activity, service, route, query, webhook, test

Optional module header: module <name>

2. Hard Keywords vs Contextual Keywords

Hard keywords cannot be used as identifiers:

Declaration: record event action validate upcast type enum import trigger schedule workflow activity service route query function webhook test

Control: let if else for in match emit fail return as true false null

Contextual keywords are plain identifiers interpreted by parser position: apply state invariant node edge start spawn branch parallel map compensate search with operation source sink credentials auth protocol data when fetch publish invoke retry_policy after host signal run request expect assert ok error status errors headers module input output timeout max_history backoff workflow on compensated index args sql gsql verify

3. Lexical Rules

4. Canonical Skeleton

module billing

type Money {
  amount: Decimal
  currency: String
}

enum Status { pending, active, cancelled }

record {
  kind "bill"
  title: String
  amount: Decimal
  status: Status = Status.pending

  state status {
    Status.pending -> Status.active on activated
    Status.active -> Status.cancelled on cancelled
  }

  invariant amount_positive {
    record.amount > 0
  }
}

event created {
  title: String
  amount: Decimal
}

validate title_ok {
  params.title != ""
}

@create
action create {
  title: String
  amount: Decimal
  validate title_ok
  apply {
    emit created { title: params.title, amount: params.amount }
  }
}

function normalize(s: String) -> String {
  return s.trim().upper()
}

5. Root Objects by Context

ContextAvailable RootsNotes
Action applyparams, record, metarecord is pre-event state
Event applyevent, record, metaevent is shorthand for evt
Upcastparams or event, metaDepends on upcast target
Validateparams, recordBoolean expression required
InvariantrecordBoolean expression required
Edge mappinginput, predecessor nodesNode names as roots
Activity data blockinput, executionexecution for runtime context
Activity applyinput, data block namesData results as roots
Route data blockinputinput.body, input.headers, input.params
Route applyinput, data block names
Trigger mappingevent, metaSource event fields
on compensatedinput, compensation, nodes
Workflow searchinputSearch attributes
Pure functionfunction parametersNo side effects
Area 1 testrecord, eventsevents is list of emitted events
Area 2 testoutput, errorWorkflow result

6. Expression Precedence (Low to High)

  1. ||
  2. &&
  3. == !=
  4. < > <= >=
  5. + -
  6. * / %
  7. ??
  8. unary ! -
  9. postfix . ?. [] and call chaining

7. Type Quick Table

SyntaxKind
Int Decimal Bool String Id Date Time DateTime Duration Epoch Value BlobPrimitives
T?Optional
List<T> or [T]List
Map<K, V> or bare MapMap (bare = Map<String, Value>)
{ field: Type, ... }Inline object type
Address or warehouse.PickResultNamed / qualified type

8. Statement Forms

9. Action/Event Lifecycle Annotations

AnnotationTargetEffect
@createactionCreates new records
@upsertactionCreates or updates
@deleteaction or eventSoft delete
@delete(hard)action or eventHard delete

10. Method Chains

String: .trim(), .lower(), .upper(), .split(sep), .replace(old, new), .contains(sub), .startsWith(prefix), .endsWith(suffix), .substring(start, end), .padLeft(len, char), .padRight(len, char), .len

List: .map(|i| expr), .filter(|i| expr), .find(|i| expr), .contains(item), .flatten(), .unique(), .sort(|i| expr), .reverse(), .first(), .last(), .sum(), .join(sep), .append(item), .len

Map: .get(key), .keys(), .values(), .entries(), .contains_key(key), .merge(other), .remove(key), .len

Numeric: .round(), .ceil(), .floor(), .abs(), .min(other), .max(other), .toString(), .toDecimal()

Date/Time: .addDays(n), .addHours(n), .addMinutes(n), .addSeconds(n), .format(pattern), .diffDays(other), .startOfMonth(), .endOfMonth(), .toEpoch(), .toDateTime()

Crypto: .sha256(), .hmac(key), .base64Encode(), .base64Decode()

Coercion: .toInt(), .toDecimal(), .toBool() (fail on error), .tryToInt(), .tryToDecimal(), .tryToBool() (return optional)

11. Constraints to Respect

12. Anti-Patterns

Anti-PatternWhy It FailsCorrect Form
emit inside a lambdaParser rejects emit outside action applyMove emit to the action apply block
record.field = valueNo mutable assignment; records change via eventsEmit an event with the new value
for loop in validateFor loops only allowed in action applyUse list methods (.filter, .find)
type inside recordTypes must be top-levelDeclare type before record
Using reserved field namespk, id, version, etc. are system-managedChoose a different name
emit without action contextEvents only emitted from actionsWrap in an action
Bare Map in record fieldsValue type not allowed in recordsUse Map<String, String> or typed map
let reassignmentBindings are immutableUse a new let binding
match without wildcard on non-enumNon-enum matches require _Add _ -> default arm

13. Generation Strategy

  1. Prefer parser-canonical forms shown in this sheet.
  2. Use contextual keywords in correct positions, not as global keywords.
  3. Keep declaration bodies ordered as parser expects (fields before apply/validate).
  4. For workflows/activities/routes, keep blocks explicit.
  5. Prefer explicit version tags (vN) when writing migration-sensitive code.
  6. Always check root object availability for the current context.
  7. Use manzano test to validate generated output.

14. Query Syntax

query list_items {
  args {
    status: String?
    limit: Int = 25
  }

  sql {
    SELECT * FROM items WHERE status = @status LIMIT @limit
  }

  output: {
    id: Id
    title: String
    status: String
  }
}

GSQL alternative:

query active_items {
  args {
    limit: Int = 50
  }

  gsql {
    items | filter status != 'cancelled' | sort created_at desc | take @limit
  }
}

Bind parameters use @name prefix. Never use :name.

15. Test Convention

Files: *_test.grove in the module directory. Discovered by manzano test.

test lifecycle {
  run create {
    request {
      name: "create_item",
      params: { title: "Test" },
      meta: { id: "item-1", by: "user-1", timestamp: 1700000000000, action_id: "cmd-1" }
    }
    expect ok
    assert record.title == "Test"
    assert events[0].name == "created"
  }
}

test validation_failure {
  run empty {
    request {
      name: "create_item",
      params: { title: "" },
      meta: { id: "bad-1", by: "user-1", timestamp: 1700000001000, action_id: "cmd-3" }
    }
    expect error "title_not_empty"
  }
}

Duration assertions use string form: assert record.prep_time == "2m" (not == 2m).

16. Route Syntax

File: apps/<app>/api/<module>/route.grove. Path from filesystem, not from code.

route {
  GET {
    data records {
      query my_module.list_items {
        limit: input.query.limit,
        offset: input.query.offset
      }
    }
  }
  POST create {
    data result {
      invoke my_module.create_item {
        title: input.body.title
      }
    }
  }
}

Dynamic segments: [id]/route.grove → input.params.id.

Input: input.body.* (POST body), input.params.* (path params), input.query.* (query string), ctx.principal (auth).

No path strings. No apply blocks. No verify blocks.

17. Web Layer

apps/<name>/
  layout.html              # Root layout ()
  index.html               # Home page
  about/page.html          # /about
  products/
    layout.html            # Nested layout for /products/*
    [id]/
      page.html            # /products/:id ()
      page.jq              # Data loader (JSON)
  api/
    todo/
      route.grove          # /api/todo
      [id]/route.grove     # /api/todo/:id

Templates: JQT syntax ``. Data context: .page (frontmatter), .params (URL), .query (query string), .data (from page.jq), .children (layout).

Subdomain: <app>.localhost:<port> serves the app. localhost:<port> serves the dev console.

18. Known Gotchas

IssueImpactWorkaround
?? null coalesceCompiles, fails at runtimeUse if conditionals
State machine co-declarationstate status {} needs matching field status: Type = defaultDeclare both
Duration assertionassert x == 2m failsUse string: assert x == "2m"
Event status fieldState event without status field won't transitionAdd status: Type = Type.value to event