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
- Comments:
// line commentsonly. No block comments. - Strings:
"..."with escapes\\ \" \n \t \r \$. - String interpolation:
"hello ${expr}". - Durations: numeric + suffix
s|m|h|d(e.g.,30s,5m). - Optional types:
T?. - Optional chain:
a?.b. - Null coalesce:
a ?? b(caution: compiles but fails at runtime — see Gotchas).
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
| Context | Available Roots | Notes |
|---|---|---|
Action apply | params, record, meta | record is pre-event state |
Event apply | event, record, meta | event is shorthand for evt |
| Upcast | params or event, meta | Depends on upcast target |
| Validate | params, record | Boolean expression required |
| Invariant | record | Boolean expression required |
| Edge mapping | input, predecessor nodes | Node names as roots |
Activity data block | input, execution | execution for runtime context |
Activity apply | input, data block names | Data results as roots |
Route data block | input | input.body, input.headers, input.params |
Route apply | input, data block names | |
| Trigger mapping | event, meta | Source event fields |
on compensated | input, compensation, nodes | |
Workflow search | input | Search attributes |
| Pure function | function parameters | No side effects |
| Area 1 test | record, events | events is list of emitted events |
| Area 2 test | output, error | Workflow result |
6. Expression Precedence (Low to High)
||&&== !=< > <= >=+ -* / %??- unary
! - - postfix
. ?. []and call chaining
7. Type Quick Table
| Syntax | Kind |
|---|---|
Int Decimal Bool String Id Date Time DateTime Duration Epoch Value Blob | Primitives |
T? | Optional |
List<T> or [T] | List |
Map<K, V> or bare Map | Map (bare = Map<String, Value>) |
{ field: Type, ... } | Inline object type |
Address or warehouse.PickResult | Named / qualified type |
8. Statement Forms
let name [: Type] = exprif ... { ... } [else { ... }]for [index,] item in expr { ... }return expremit event_name [vN] { fields }fail code_expr message_exprpublish target { fields }- Expression statement
- Spread in emit/publish/object:
...expr
9. Action/Event Lifecycle Annotations
| Annotation | Target | Effect |
|---|---|---|
@create | action | Creates new records |
@upsert | action | Creates or updates |
@delete | action or event | Soft delete |
@delete(hard) | action or event | Hard 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
- At most one unnamed root
record { ... }per module; it must declarekind "xxx"as the first item inside the body. kindvalues are 2-5 lowercase ASCII letters, never start withmz(reserved), and must be unique per project.emitis allowed only in action apply context; forbidden in lambdas.forloops allowed only in action apply; max nesting depth 3.- Validate references in actions must exist.
validatebody must be boolean.- Upcast source version must be lower than target version.
@create/@deleteactions are for root records.- Reserved runtime fields cannot be user-declared:
pk id version created_at updated_at created_by updated_by.
12. Anti-Patterns
| Anti-Pattern | Why It Fails | Correct Form |
|---|---|---|
emit inside a lambda | Parser rejects emit outside action apply | Move emit to the action apply block |
record.field = value | No mutable assignment; records change via events | Emit an event with the new value |
for loop in validate | For loops only allowed in action apply | Use list methods (.filter, .find) |
type inside record | Types must be top-level | Declare type before record |
| Using reserved field names | pk, id, version, etc. are system-managed | Choose a different name |
emit without action context | Events only emitted from actions | Wrap in an action |
Bare Map in record fields | Value type not allowed in records | Use Map<String, String> or typed map |
let reassignment | Bindings are immutable | Use a new let binding |
match without wildcard on non-enum | Non-enum matches require _ | Add _ -> default arm |
13. Generation Strategy
- Prefer parser-canonical forms shown in this sheet.
- Use contextual keywords in correct positions, not as global keywords.
- Keep declaration bodies ordered as parser expects (fields before apply/validate).
- For workflows/activities/routes, keep blocks explicit.
- Prefer explicit version tags (
vN) when writing migration-sensitive code. - Always check root object availability for the current context.
- Use
manzano testto 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
| Issue | Impact | Workaround |
|---|---|---|
?? null coalesce | Compiles, fails at runtime | Use if conditionals |
| State machine co-declaration | state status {} needs matching field status: Type = default | Declare both |
| Duration assertion | assert x == 2m fails | Use string: assert x == "2m" |
| Event status field | State event without status field won't transition | Add status: Type = Type.value to event |