Reference

Aktion language.

A compact, line-oriented language designed for LLMs. A program is a flat list of name = expression statements. The parser is error-tolerant — invalid lines are dropped and valid lines render immediately as they stream in.

Mental model

Aktion has a single program shape: a list of named bindings. The reserved name _app_ is the entry point — every response MUST begin with _app_ = ... on the first line. Every other binding is referenced from _app_ directly or transitively. Re-render happens automatically whenever a reactive atom changes; there is no manual subscription model.

_app_ = Stack([header, dashboard])

header = SectionHeader("Acme Dashboard")

dashboard = Card([
  CardHeader("Active users", subtitle: "Last 7 days"),
  Text(@Format($activeUsers))
])

$activeUsers = 12540

Three identifier conventions cooperate:

Statements

KindSyntaxNotes
Root assignment_app_ = ExprReserved entry point; required, first line.
Bindingname = ExprPlain identifier — non-reactive alias, can reference other bindings (forward refs OK).
Reactive atom$name = ExprMutable, triggers re-render on change.
Component declcomponent Name(args) { … return Expr }User-declared component with per-instance state. MUST end with explicit return.
Action declaction Name(args) { steps }Imperative block triggered by events. MAY return.
Effect decleffect [ ...deps ] { steps }Anonymous declarative side effect. Dependencies ($atom, on:mount, on:unmount, on:every(N), debounce(N), throttle(N)) live in the bracketed list. effect { … } is equivalent to effect [on:mount] { … }.
Emitemit "name" { detail }Dispatch a CustomEvent on the host (inside action/effect bodies).
Theme overridetheme = Theme({ tokens })Top-level binding named theme — the runtime detects it and applies the tokens as CSS custom properties.
i18n config$i18n = i18n({ … })Configures internationalization — messages, locale, fallback.
HTTP defaults$http = Http({ … })Optional response-wide defaults (base URL, headers, retry).
Comment# textWhole-line comment; ignored by the parser.

Statements are line-oriented — newline terminates a statement. Never use semicolons or statement-level commas. Expressions may span multiple lines as long as they sit inside an unmatched [, (, or {.

Expressions

# Arithmetic + ternary + reactive read
greeting = "Hi, " + ($user.name ?? "stranger") + "."
score    = $hits * 10 + $bonus
status   = $isActive ? "online" : "offline"
summary  = `${@Count($orders)} ${@Plural(@Count($orders), "order", "orders")} this week`

Array shortcuts

Reactive state

Aktion has one reactive atom kind. There is no $state, $persist, $session, or $computed keyword. Every reactive cell is declared and read with the same surface:

$count = 0
$user  = { name: "Ada", role: "Engineer" }
$todos = []
$theme = "dark"

action increment() {
  $count = $count + 1
}

Sigil contract

Assignment rules

Component-scoped state

A $name = value declared inside a component body is per-instance. Two UserCard siblings each have their own $hover. Top-level $name declarations live for the lifetime of the response.

Computed values

There is no dedicated $computed keyword. Just compute:

$cart  = []
$total = @Sum($cart.price)        # re-derives when $cart changes
$open  = @Filter($todos, "done", "==", false)

Persistence

Persistence lives on the storage global (see Built-in globals). Read and write directly:

$theme = storage.get("theme") ?? "light"

action toggleTheme() {
  $theme = $theme == "light" ? "dark" : "light"
  storage.set("theme", $theme)
}

HTTP — the network primitive

There is exactly one network primitive: http({ url, method, headers, body, query, … }). Pass any fetch-compatible option plus a convenience query object that is serialised into the URL. The result is a reactive resource bag.

$orders = http({
  url:    "/api/users/" + $userId + "/orders",
  method: "GET",
  query:  { limit: 5, status: "open" },
  headers:{ "X-Tenant": $tenant }
})

Reactive resource shape

$orders.data         # parsed response body (null until resolved)
$orders.error        # null on success
$orders.status       # HTTP status code, e.g. 200
$orders.loading      # true while in-flight
$orders.headers      # response headers as a plain object
$orders.lastUpdated  # ms-epoch of last successful response
$orders.refetch()    # re-issue the request
$orders.cancel()     # abort in-flight request

Async helper

Use the Async helper component to branch on a resource's state:

_app_ = Async($orders,
  loading: LoadingState("Loading orders…"),
  error:   ErrorState("Couldn't fetch orders"),
  empty:   EmptyState("No orders yet"),
  data:    Table([Col("Item", $orders.data.title), Col("Total", $orders.data.total, format: "currency")])
)

Mutations

Mutations look identical — fire from inside an action body and observe the resource:

action saveOrder(payload) {
  $save = http({ url: "/api/orders", method: "POST", body: payload })
  emit "assistant-message" { message: "Saved." }
}

Host-wide defaults

$http = Http({
  baseUrl: "https://api.example.com",
  headers: { "Accept": "application/json" },
  timeout: 10000,
  retry:   { count: 2, backoff: "exponential" }
})

Components and lambdas

component UserCard(user, tone: "default") {
  $hover = false
  return Card([
    Avatar(user.name, size: "md"),
    Text(user.name, variant: "large-heavy"),
    Text(user.role, tone: "muted"),
    Badge(tone, tone: tone)
  ])
}

_app_ = Stack([
  UserCard($alice),                                  # positional arg
  UserCard($bob, tone: "primary"),                   # named arg
  UserCard(user: $carol, tone: "warning")            # both named
])

Local helpers — lambda form

Use a lambda binding for one-off helpers that don't need their own component:

priorityTone = (p) => match p { "high": "danger" "med": "warning" default: "muted" }
rowFor       = (item) => Stack([Badge(item.label, tone: priorityTone(item.priority)), Text(item.title)])
list         = for item in $items { rowFor(item) }

Actions

Actions are imperative blocks invoked from event-bearing components (Button(action:), Button(onClick:), form submits, …). They run in order; state writes batch into a single re-render.

action addTask(title) {
  $tasks = [...$tasks, { id: $tasks.length + 1, title: title, done: false }]
  $newTaskTitle = ""
}

action submit() {
  $save = http({ url: "/api/tasks", method: "POST", body: { title: $newTaskTitle } })
}

For the full body grammar and patterns, see the Actions guide.

Effects

Effects are declarative, anonymous side effects. Every dependency lives in a single bracketed list right after the effect keyword — there is no name and no on keyword. Use them for polling, analytics, hydration, and one-shot setup.

# Mount-once: run when the app first renders
effect [on:mount] {
  $session = http({ url: "/api/session", method: "GET" })
}

# Reactive: re-run whenever $query changes (debounced 300ms)
effect [$query, debounce(300)] {
  $results = http({ url: "/api/search", method: "GET", query: { q: $query } })
}

# Interval: run every 5 seconds
effect [on:every(5000)] {
  $now = @Now()
}

# Empty list — equivalent to `effect [on:mount] { … }`
effect {
  console.log("mounted")
}

A dependency entry is one of:

The order inside the brackets does not matter. effect { … } with no brackets and effect [on:mount] { … } are equivalent.

Use cleanup(fn) inside an effect body (typically from js{}) to register teardown for intervals, listeners, or observers.

Control flow

All three control-flow keywords are expressions — they yield a value (or array of nodes) that can be assigned, passed as a prop, or returned from a component / action body.

if / else

banner = if $hasError { Banner("Something went wrong", tone: "danger") } else { null }
active = if $tab == "billing" { billingPanel } else { overviewPanel }

A trailing else is optional — without it an unmatched if evaluates to null (renders nothing).

match

panel = match $stage {
  "draft":     DraftView()
  "review":    ReviewView()
  "shipped":   ShippedView()
  default:     EmptyState("Pick a stage")
}

for

rows = for item in $todos { TaskRow(item) }
rowsWithIndex = for (item, idx) in $todos { TaskRow(item, index: idx) }

for produces an array of nodes — assign it and reference the binding from a container (Stack(rows), Table([Col("Task", rows)])). The loop variable is block-scoped, so a stale closure can never see the wrong row.

Statement form inside action / effect bodies

action submit(payload) {
  if !payload.email { return }
  for tag in payload.tags { $tags = [...$tags, tag] }
  match payload.kind {
    "draft": { $drafts = [...$drafts, payload] }
    default: { $records = [...$records, payload] }
  }
}

Two-way binding

Two-way binding is implicit: pass a $variable (or a member chain rooted at one) as the value of an input prop and the runtime wires the change handler automatically.

$search = ""
bar     = SearchBar("q", placeholder: "Search…", value: $search)
list    = for row in @Filter($rows, "title", "contains", $search) { ListItem(row.title) }

$tags = []
chips = TagInput("tags", value: $tags)

# Member chains rooted at a $variable bind deeply — the runtime rebuilds
# the root object immutably so subscribers always wake up.
$form    = { email: "", remember: false }
emailIn  = Input("email",    value: $form.email)
remember = Switch("remember", value: $form.remember)

Any form control whose spec declares a primary value prop participates: Input, TextArea, Select, Combobox, MultiSelect, Checkbox, CheckBoxGroup, Switch, ToggleGroup, Slider, NumberInput, DatePicker, DateRangePicker, TimePicker, DateTimePicker, SearchBar, PinInput, PasswordInput, TagInput, MentionInput, MaskedInput, RichTextEditor, CodeEditor, ColorPicker, Rating (when interactive: true), and Pagination.

Built-in @-functions

Builtins are pure, prefixed with @, and never have side effects. Use them for data shaping, formatting, and inline iteration. The runtime ships 50+; the catalogue below covers what's available.

Data & aggregation

Formatting

Date / time helpers

Math & numeric utility

Iteration & conditional fallbacks

The blessed surface is the expression-form for / if / match covered above. The @-form survives for tight inline expressions:

Built-in globals

Two namespace globals are always in scope — no import, no declaration. Invoke them through the standard obj.method(args) syntax. Named-arg options collapse into a single trailing options object.

storage — browser storage

storage.set("name", "John")           # alias of storage.local.set
$name = storage.get("name")
storage.remove("name")
storage.clear()

storage.session.set("draft", $draft)  # per-tab sessionStorage
$draft = storage.session.get("draft")

storage.cookies.set("user", "John", expires: 7, path: "/", sameSite: "Lax")
$user = storage.cookies.get("user")
storage.cookies.remove("user", path: "/")

console — host console forwarder

console.log("Hello", $user)
console.error("Failed", $error)
console.warn("Deprecated path")
console.info("Route changed", _route_.path)
console.debug({ days: $days, count: $count })

Routing — _router_({ … })

The router is a plain function call. It returns the matched arm's evaluated value:

pages = _router_({
  "/":             Dashboard(),
  "/orders":       OrdersPage(),
  "/orders/:id":   OrderDetail(id: params.id),
  "/settings/*":   SettingsArea(rest: params._),
  default:         NotFound()
})

_app_ = AppShell(MainSidebar(), pages)

See the Routing guide for path patterns, params, query strings, and NavLink.

Internationalization

$locale = storage.get("locale") ?? "en"
$bundle = http({ url: "/i18n/" + $locale + ".json", method: "GET" })
$i18n   = i18n({
  locale:   $locale,
  messages: $bundle.data ?? {},
  fallback: "en"
})

Text(t("orders.title"))                          # "Orders"
Text(t("orders.greeting", { name: $userName }))  # "Welcome back, Alex"

JavaScript escape hatch — js{ … }

js{ /* opaque JS body */ } runs raw JavaScript inside an effect, action, or lambda body. Use sparingly — every other surface is preferred — but it is always available for browser APIs not exposed natively (clipboard, keyboard listeners, IntersectionObserver, audio, custom DOM work). See the JavaScript interactions guide for the full ctx surface.

Markup escape hatches — HTMLTag & Styles

When the standard component catalogue cannot express the markup or styling you need, two last-resort components are available. Reach for them only after confirming nothing in the standard library captures the design — the dedicated components produce a more consistent UI for fewer tokens.

_app_ = Stack([
  Styles(`
    .hero-callout { background: linear-gradient(135deg, #6366f1, #10b981); color: white; padding: 24px; border-radius: 12px; }
    .hero-callout h2 { margin: 0 0 8px; }
  `),
  HTMLTag("div", attributes: { class: "hero-callout" }, children: [
    HTMLTag("h2", children: [Text("Custom block")]),
    Text("Use HTMLTag + Styles only when the standard components cannot capture the design.")
  ])
])

In-script theming

Assign a Theme({ … }) call to a top-level binding named theme — the runtime detects that exact name and applies the tokens as CSS custom properties on the host:

theme = Theme({
  colors: {
    primary: "#635bff",
    bg:      "#0a0a23",
    surface: "#10103a",
    text:    "#ffffff"
  },
  radius: { button: "999px" },
  font:   { family: "'Inter', sans-serif" }
})

_app_ = AppShell(...)

The top-level keys must be one of colors, radius, font, motion, elevation (plus the metadata keys name and direction). See the Themes guide for the full token taxonomy.

Icons (Font Awesome)

Icon-typed props accept a Font Awesome name as a string. The host element auto-loads the Font Awesome stylesheet — no setup needed.

Comments & whitespace

Hoisting & streaming

Aktion supports hoisting: a reference can be used before it is defined. The renderer re-parses the program on every streamed chunk and silently treats unresolved references as empty, so a partially-streamed response renders progressively from the top.

Required statement order for streaming-friendly output:

  1. _app_ = ... — emit this FIRST so the UI shell appears immediately.
  2. component / action / effect declarations — fill in layout & behaviour.
  3. Leaf data values (strings, numbers, arrays, objects) — last.

Error tolerance

Next