ackee
Docs/Ackee Script

Syntax

Files, comments, declarations, fields and values — and the grammar.

An Ackee Script file is a list of declarations. Each declaration is a keyword, a name, and a body of fields. There are no expressions to evaluate: every value is either written out or refers to a value stored in the environment, so a file reads the same to every person and every tool.

ackee.ack
ackee 1
​
# Comments run from # to the end of the line.
project "shop" { cloud = "aws"; region = "us-east-1" }
​
node "net" type "aws/vpc@1" {
name = "main"
cidr = "10.0.0.0/16"
azCount = 2
}
​
node "jobs" type "aws/worker@1" {
name = "jobs" # a string
image = "public.ecr.aws/docker/library/alpine:3.21"
cpu = 256 # a number
memory = 512
desiredCount = 1
command = ["sh", "-c", "sleep 3600"] # a list
​
envVars = { # a map
LOG_LEVEL = var.LOG_LEVEL # a reference
WORKERS = "4"
}
}
​
connect network "net" -> "jobs"

Files

A script is a UTF-8 text file. In a repository it is called ackee.ack and lives at the root unless you point Ackee elsewhere. Its very first line is the language version, ackee 1 — before any comment — so that a reader built for a later version can tell what it is reading, and one built for an earlier version refuses rather than guesses (AK101, AK102).

Comments

# starts a comment, which runs to the end of the line. A comment on the lines above a declaration or a field belongs to it; a comment after a value on the same line belongs to that field. When Ackee edits your file — because you changed something on the canvas — it keeps both kinds where you put them, along with the order of your fields and the blank lines between them.

That holds inside a map or a list too, and when Ackee rewrites a declaration because one of its values changed: every other field keeps its place and its comments, and only the changed value reads differently.

Declarations

A declaration is a keyword, one or more quoted names, an optional type "...", and a body in braces:

the shape of a declaration
service "web" {
image = "public.ecr.aws/nginx/nginx:latest"
}
​
node "uploads" type "aws/s3-bucket@1" {
name = "uploads"
}

The keyword says what kind of thing is declared. The name is how the rest of the file refers to it and how Ackee recognises it on every later deploy, so two declarations cannot share one (AK207). A body may be empty: database "main" {} takes every default.

connect is the one declaration without a body. It joins two declarations and takes the whole line:

a connection
connect env "web" -> "main"

The keywords in version 1 are:

KeywordDeclares
projectthe cloud, region and environment — exactly one per file
servicea container that runs your code (portable)
databasea managed Postgres or MySQL database (portable)
workera container that keeps running without answering requests (portable)
joba container that runs to completion when started (portable)
cachea managed Redis (portable)
bucketobject storage (portable)
queuea work queue (portable)
secreta secret in your cloud's secret manager (portable)
registrya private container registry (portable)
sitea static website (portable)
sourcea repository that builds and deploys on push
nodeany resource in the catalogue, exactly
connecta typed connection between two declarations

Fields

Inside a body, each field is name = value. Fields are usually one per line; a semicolon or a comma also separates them, which lets a short block sit on one line:

three fields on one line
project "shop" { env = "dev"; cloud = "aws"; region = "us-east-1" }

Without one of them, two fields on a line are an error (AK114) rather than a guess — and the same goes for two declarations.

A field name is a bare word — letters, digits, _ and -, not starting with a digit — or a quoted string when it needs other characters, such as an environment variable name that is not a valid word. A field may appear only once in a body (AK115): a later line never silently overrides an earlier one.

Values

KindWrittenNotes
string"us-east-1"Double quotes, on one line. Escapes: \" \\ \n \t \uXXXX.
number80 · 0.5 · -1 · 1e3Written as JSON writes them — not 080 or 30. — and stored as written: 1.0 stays 1.0.
booleantrue · falseBare words, not strings.
list["sh", "-c", "make"]Items separated by commas or line breaks.
map{ LOG_LEVEL = "info" }The same shape as a body: name = value.
referencevar.LOG_LEVEL · secret.API_KEYA value stored in the environment, not in the file. See Values and secrets.

Anything else is not a value. In particular a bare word is not a string — region = us-east-1 is an error that asks for quotes (AK111) — and there is no null: leave a field out to take its default.

No expressions

Version 1 has no arithmetic, no string interpolation, no conditionals and no function calls. A file that computes is a program, and a program can mean something different to the tool than to the person reviewing it. Values that differ between environments come from var.NAME instead, resolved by Ackee when it plans a deploy.

Reserved words

when, if, on, every and run cannot start a declaration (AK107). They are kept for a later version, so files written today keep working when they arrive.

Formatting

Ackee writes files in one canonical layout: two-space indentation, one blank line between declarations, and the = of neighbouring fields aligned. A blank line or a value that spans several lines starts a new alignment group. You may lay a file out however you like — layout never changes what a file means — and when Ackee edits a file it only rewrites the declarations whose meaning changed, leaving every other line exactly as you wrote it.

Grammar

The complete syntax of version 1, in EBNF. Separators between fields and between list items are optional where the grammar shows them inside a repetition: the reader accepts [1 2 3] as readily as [1, 2, 3], though Ackee always writes the second.

Ackee Script 1 — EBNF
file         = "ackee" version eol { declaration } ;
version      = digit { digit } ;

declaration  = block | connection eol ;
block        = keyword { string } [ "type" string ] "{" body "}" ;
connection   = "connect" name string "->" string [ "id" string ] ;

body         = { field | "," | eol } ;
field        = ( name | string ) "=" value ;

value        = string | number | "true" | "false"
             | list | map | reference ;
list         = "[" { value | "," | eol } "]" ;
map          = "{" body "}" ;
reference    = ( "var" | "secret" ) "." ( name | string ) ;

keyword      = name ;
name         = ( letter | "_" ) { letter | digit | "_" | "-" } ;
number       = [ "-" ] ( "0" | nonzero { digit } )
               [ "." digit { digit } ] [ ( "e" | "E" ) [ "+" | "-" ] digit { digit } ] ;
string       = '"' { char | escape } '"' ;     (* on one line *)
eol          = newline | ";" ;
comment      = "#" { any character but newline } ;   (* at the end of any line, or on its own *)