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 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 stringimage = "public.ecr.aws/docker/library/alpine:3.21"cpu = 256 # a numbermemory = 512desiredCount = 1command = ["sh", "-c", "sleep 3600"] # a listenvVars = { # a mapLOG_LEVEL = var.LOG_LEVEL # a referenceWORKERS = "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:
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:
connect env "web" -> "main"
The keywords in version 1 are:
| Keyword | Declares |
|---|---|
project | the cloud, region and environment — exactly one per file |
service | a container that runs your code (portable) |
database | a managed Postgres or MySQL database (portable) |
worker | a container that keeps running without answering requests (portable) |
job | a container that runs to completion when started (portable) |
cache | a managed Redis (portable) |
bucket | object storage (portable) |
queue | a work queue (portable) |
secret | a secret in your cloud's secret manager (portable) |
registry | a private container registry (portable) |
site | a static website (portable) |
source | a repository that builds and deploys on push |
node | any resource in the catalogue, exactly |
connect | a 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:
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
| Kind | Written | Notes |
|---|---|---|
| string | "us-east-1" | Double quotes, on one line. Escapes: \" \\ \n \t \uXXXX. |
| number | 80 · 0.5 · -1 · 1e3 | Written as JSON writes them — not 080 or 30. — and stored as written: 1.0 stays 1.0. |
| boolean | true · false | Bare 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. |
| reference | var.LOG_LEVEL · secret.API_KEY | A 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.
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 *)