Common (CL) reference

Everything the language does, in the order you need it. Every example on this page is real Common and would run as written.

1. How a page works

A Common page is an ordinary HTML file. Code goes inside <cl> tags. When a visitor asks for the page, the server runs each block and replaces it with what the block said. The visitor only ever receives HTML.

<h1>Hello, <cl>show query "name"</cl>!</h1>

<cl>
  set fruits to "apple", "banana"
  for each fruit in fruits
    show "<li>[fruit]</li>"
  end
</cl>

Three rules hold everywhere:

  • One statement per line. No semicolons. A full stop at the end of a line is fine, and the word the is silent, so set total to the count of fruits. reads as a sentence and means set total to count of fruits.
  • Words are lowercase. Say is an error that tells you to write show.
  • Blocks end with end. No braces, no indentation rules. Indent anyway, for the reader.

A block's final newline is dropped, so Hello, <cl>show name</cl>! reads "Hello, Ada!" with no gap. Comments start with # and run to the end of the line.

Longer text: a block

When the text is longer than the command, especially HTML, write show on its own line, the text as typed underneath, and end. No quotes, real line breaks, double quotes allowed, and brackets still fill in values. set NAME to works the same way.

for each post in posts
  show
    <article>
      <h2>[title of post]</h2>
      <p>[first of paragraphs of body of post]</p>
    </article>
  end
end

Only a line that is exactly end closes the block, so the text may contain any other words, including if or show.

Sharing parts of pages

include "header.html"

The included file can contain its own <cl> blocks and sees every variable you have set so far. This site's header, footer and settings are includes.

Showing code instead of running it

<cl-code>show "shown, not run"

Anything inside <cl-code> is escaped and highlighted: Common's words get class="cl-word", text gets cl-text, brackets cl-hole, comments cl-note. Every example on this page is one.

2. Values

Common has five kinds of value. You never declare which one you have; the value knows.

KindLooks likeNotes
Text"Hello"Always in double quotes. May contain brackets.
Number42, 4.5, -3Whole or decimal. Shown without trailing zeros: 4.50 prints as 4.5.
Answeryes, noWhat a condition gives. Prints as the word.
List"a", "b", "c"Items in order, counted from 1. Prints as a, b, c.
Recordone row from the databaseNamed columns. name of row reads one. Prints as name: Ada, age: 36.

Text that came from a visitor or the database is marked, and is made safe when shown. See safety.

3. Brackets: values in text

This is the one idea in Common with no exact match in other languages, so it gets its own section. To put a value inside text, write it in square brackets inside the quotes.

set planet to "Earth"
set moons to 1
show "Welcome to [planet], home of [moons] moon."

The sentence stays a sentence. The spaces are the spaces of the sentence, so you cannot get them wrong.

Anything that is a value can go inside

show "You have [count of messages] messages."
show "Half price: $[price / 2]"
show "Second colour: [item 2 of colours]"
show "Hello, [query "name"]!"
show "Is 7 at most 7? [7 is at most 7]"
show "Slug: [replace " " with "-" in lowercase of title]"

A hole may contain quotes of its own, as in [query "name"]. The interpreter finds the matching bracket, not the next quote.

What goes in comes out as text

A number prints without trailing zeros. A list prints as a, b, c. An answer prints as yes or no. A record prints as name: Ada, age: 36.

Building text up

There is no + for text. To add to a piece of text, put the old text in a hole:

set missing to ""
if name is empty
  set missing to "[missing]<li>Your name</li>"
end

A real bracket, a real quote, a new line

show "A literal \[bracket], a \"quote\", and\na new line"

\[, \", \n and \\ are the four escapes. You rarely need them in HTML.

Brackets and HTML

Your own HTML inside the quotes is sent as HTML. A value in a hole that came from a visitor is escaped. So this is safe even when name contains a <script> tag:

show "<h1>Hello, [name]</h1>"

Mistakes it catches

  • "a" + 1: '+' adds numbers. To put a value in text, write it in brackets.
  • "Hello [name": the text has a [ with no closing ].
  • "Hello []": the brackets in the text are empty.
  • show [x] outside quotes: brackets only work inside text.

4. Variables

set name to "Ada"
set total to price * 3
set tags to "new", "sale"
set names to empty list

set NAME to VALUE creates the variable or replaces it. Names are letters, digits and underscores: total_price, page2. A name cannot be a command or a word that stands alone, like show, today or yes. Words that are only used before of, such as count, year or date, may also be names: set year to 2026 and year of today live side by side.

A variable is readable anywhere later on the same page, including inside included files and inside loops. There is no way to declare one without a value, which is deliberate.

5. Math

set subtotal to price * quantity
set tax to subtotal * 0.2
set total to subtotal + tax
show "[(2 + 3) * 4]"
show "[10 / 4]"

+ - * / work on numbers only, with the usual precedence and parentheses. 10 / 4 is 2.5. Dividing by zero is an error. To do math on something a visitor typed, turn it into a number first: number of query "qty".

6. Conditions

if age is at least 18
  show "Welcome"
otherwise if age is more than 12
  show "Almost"
otherwise
  show "Not yet"
end

Comparisons are English and there is one form for each:

a is bequala is not bnot equal
a is more than bgreatera is less than bsmaller
a is at least bgreater or equala is at most bsmaller or equal
a is between 1 and 10from 1 to 10, inclusivea is not between 1 and 10outside that range
a is emptyempty text, empty list or zeroa is not emptyhas something in it

Numbers compare as numbers. Text compares as text, so "10" is more than "9" is no; use number of first. Join conditions with and, or and not; and binds tighter than or, and parentheses group.

if name is not empty and (paid or member)
  show "In you go"
end

7. Loops

for each fruit in fruits
  show "<li>[fruit]</li>"
end

repeat 5 times
  show "★"
end

for each takes a list and runs the block once per item, with the item in the variable you named. repeat takes a number. Loops nest as deep as you like, and the loop variable keeps its last value afterwards. There is no while: Common pages are short-lived, and a loop that might never end has no place in one.

Counting as you go

set position to 0
for each name in names
  set position to position + 1
  show "[position]. [name]"
end

Walking two lists together

set titles to "Plain English", "Server-side", "Tiny"
set blurbs to "If you can read it, you can write it.", "Like PHP.", "One C file."
set position to 0
for each heading in titles
  set position to position + 1
  show "<h3>[heading]</h3><p>[item position of blurbs]</p>"
end

Stopping early

set found to no
repeat 100 times until found
  set line to first 10 of line
  if count of line is less than 10
    set found to yes
  end
end

repeat N times until CONDITION stops as soon as the condition is yes. The number stays as the ceiling, so a page can never run forever. There is no while.

8. Lists

set colours to "red", "green", "blue"
add "yellow" to colours
show "[count of colours]"
show "[first of colours], [last of colours], [item 2 of colours]"
show "[colours]"

Positions count from 1. item 5 of colours on a four-item list is an error that tells you how many there are. count of on text gives the number of letters. Lists can hold any mix of values, including records from the database.

8b. Records you make

A record keeps named values together. Until now records only came from the database; you can make your own.

set lamp to record
  name: "Desk lamp"
  price: 19
  stock: 7
end
show "[name of lamp] costs $[price of lamp]"
set price of lamp to 15
set key to "stock"
show "[item key of lamp] left"
add lamp to products

Read a column with price of lamp, change one with set price of lamp to 15, and look one up by a name held in text with item key of lamp, the same item that picks a list item by number.

8c. Verbs: your own commands

A verb is a recipe you name once and call like any built-in command. It is the sixth fundamental, and it is written with the block shape you already know.

to greet someone
  show "<p>Hello, [someone]!</p>"
end

to slug_of heading
  give replace " " with "-" in lowercase of trim of heading
end

greet "Ada"
greet query "name"
show "[slug_of title]"
  • The names after the verb's name are the values it takes. Call it with one value per name.
  • give VALUE hands a value back and finishes the verb. A verb that gives can be used wherever a value goes: in brackets, after set … to, in a condition.
  • A verb can read every variable set before the call. What it sets inside stays inside; the way out is give.
  • A verb in an included file works on every page that includes it. That is how a site grows its own words.

9. Text words

uppercase of xHELLO
lowercase of xhello
trim of xno spaces at the ends
split x by "/"a list of the pieces; empty pieces are skipped
replace "a" with "b" in xevery a becomes b
paragraphs of xa list, split on blank lines, each trimmed
number of xthe text as a number, or an error if it is not one
count of xhow many letters
first 100 of x, last 3 of xthe first or last N letters (or items of a list)
x contains "@"yes if the piece is inside
sort of list, reverse of listin order, or back to front
date of x, year of x, month of x, day of x24 October 2026, 2026, October, 24
set slug to replace " " with "-" in lowercase of trim of title
set parts to split "2026-10-24" by "-"
show "Year [item 1 of parts]"
for each paragraph in paragraphs of body
  show "<p>[paragraph]</p>"
end

These read right to left when chained: lowercase of trim of title trims first, then lowercases.

10. The visitor

query "name"what came after ?name= in the address. Empty text if missing.
form "name"a field from a submitted form with method="post". Empty text if missing.
paththe address after the site name: /blog/hello-world
today2026-10-24
now2026-10-24 18:30

A form, start to finish

set name to trim of form "name"
set message to trim of form "message"
set problem to ""

if form "sent" is "yes"
  if name is empty
    set problem to "Please tell us your name."
  otherwise if count of message is less than 3
    set problem to "Say a little more than that."
  otherwise
    tell database "insert into guestbook (name, message, posted) values (?, ?, ?)" with name, message, now
    go to "/guestbook?thanks=yes"
  end
end

The hidden sent field tells the page whether the form was submitted or merely shown. After a successful write, go to redirects, so refreshing the page does not submit twice.

go to "/thanks"

go to sends the visitor to another address and stops the page. Nothing after it is sent.

The server does three things without any words:

  • Clean addresses. /about serves about.html. /blog/hello-world serves blog.html, and the page reads the rest with last of split path by "/".
  • A custom not-found page. Put a 404.html in the folder and it is shown, with a real 404 status, for any address that does not exist.
  • Files that are not pages, such as CSS and images, are sent as they are. Database files, cl.secret and saved emails are never sent.

12. The database

Common reads and writes SQLite, a database that is one file next to your pages, and, when built with the Postgres client library, a Postgres server through the same words: open database "postgres://user:password@host/dbname". The SQL inside the quotes is real SQL, not Common; this is the one place a second language lives inside the first, and it is deliberate.

open database "site.db"

set posts to ask database "select slug, title, body from posts order by posted desc"
show "[count of posts] posts"

set post to first of posts
show "<h1>[title of post]</h1>"

set found to ask database "select * from posts where slug = ?" with slug
if count of found is 0
  show "No such post"
end

tell database "insert into notes (text, posted) values (?, ?)" with note, now
open database "file.db"once per page, before asking or telling
ask database "SQL" with a, ba query; gives a list of records
tell database "SQL" with a, binsert, update, delete, create
column of recordone value; any column name works, even count

Each ? in the SQL takes the next value after with, sent to the database separately from the SQL. A visitor cannot change the query through the address bar. The number of values must match the number of question marks, or Common says so.

Text from the database is treated like visitor text and made safe when shown. Numbers come back as numbers. An empty column comes back as empty text.

13. Visitors who sign in

A value you remember is kept for that one visitor and comes back on every later page until they forget it. Under the hood it is a signed cookie: nobody can forge it, because it is signed with a secret Common creates in cl.secret on first use and never serves.

remember "user" as name
show "Signed in as [remembered "user"]"
forget "user"
forget everything

Passwords

hash of turns a password into a salted one-way scramble you can store. matches checks a typed password against a stored hash. You never store or compare the password itself.

# when someone registers
tell database "insert into users (name, password) values (?, ?)" with name, hash of form "password"

# when they sign in
set users to ask database "select name, password from users where name = ?" with name
if count of users is 1 and form "password" matches password of first of users
  remember "user" as name
  go to "/admin"
end

# on any page only for signed-in people
if remembered "user" is empty
  go to "/login"
end

Remembered values are text. Keep them small, a name or an id. The cookie lasts 30 days, cannot be read by JavaScript, and is not sent on cross-site POSTs. The sign-in page and the admin page are built on these lines.

13b. Email

send email sends a plain-text email to an address you wrote yourself. A visitor's value is refused as the recipient, so nobody can use your form to send email to anybody else. The visitor's address goes in reply to:, after checking it with is an email.

if email is not an email
  set problem to "Please check your email address."
otherwise
  send email to "owner@example.com"
    subject: "Message from [name]"
    reply to: email
    from: "website@example.com"
    body: "[name] wrote:\n\n[message]"
  end
  go to "/contact?thanks=yes"
end

While you build with cl serve, emails are saved as files in a private outbox/ folder instead of being sent. Under cl cgi on a real host they go through sendmail. A page sends at most 3 emails each time it runs, and a site at most 20 an hour. Line breaks in a subject become spaces, so a visitor cannot add a header such as Bcc:. The contact page is a working example.

14. Safety

Five sources are untrusted: query, form, path, remembered, and text from the database. A value from any of them is HTML-escaped whenever it is shown or placed into text with brackets, so a <script> tag typed by a visitor is shown as the text <script>. Your own quoted HTML is never escaped. You cannot forget to do this, because there is nothing to do.

show raw value turns the safety off for one value. Use it only for HTML you wrote or stored yourself.

Database values are bound, never pasted into SQL. The server refuses to send database and secret files (.db, .sqlite, .sql, .secret), saved emails (outbox/, .eml, mail.log) and .log files, whatever their case, and refuses any address containing ... A form posted to your site from another site is refused too.

A visitor's value may be shown, never run. Common remembers which text a visitor's value was filled into, and refuses it where it would be dangerous, each time naming what to write instead:

ask database "... where slug = '[slug]'"refused: write ? and put slug after with
include query "page"refused: a visitor could read any file. Write the name yourself
open database query "db"refused, for the same reason
go to query "back"allowed only for a page on this site, starting with /
send email to form "to"refused: write the address yourself. The visitor's address goes in reply to:

Sessions are a signed cookie, so a visitor cannot edit what is remembered. Passwords are stored as salted hashes with hash of. Each request runs in its own process with a 30-second limit (on Windows, the development server runs one request at a time with no limit), and practice code runs in a sandbox with no files, database, cookies or email.

15. Errors

An error appears in red where the block would have been, with the line number in the HTML file. Output printed before the error is kept. Common does not guess: a wrong word is an error that names the right word.

You wroteCommon says
show Hello, world!the words after 'show' need to be in double quotes: show "Hello, world!"
print Hello'print' is not a Common word. Use 'show', and put the words in double quotes: show "Hello"
sey "x"I do not understand 'sey'. Did you mean 'set'?
Show "x"Common words are always lowercase. Write 'show'
say "x"'say' is not a Common word. Use 'show': show "x"
x = 5Common has no '='. Write 'set NAME to VALUE' to store, or 'is' to compare
if a > 2Common has no '>'. Write 'is more than'
else'else' is not a Common word. Use 'otherwise'
set show to 1'show' is a Common word: show VALUE, so it cannot be a name
set x 5expected 'to'. The whole line is written: set NAME to VALUE
show totlI do not know what 'totl' is. Did you mean 'total'?
a missing endthis 'if' is never closed. Add a line with just 'end' after the lines it decides about
"ten" * 2can only multiply numbers, not text and a number

The errors page runs mistakes like these live.

16. Every word

This is the whole language, read from the interpreter as this page loads. cl words prints the same list.

FormMeaning
"text with [a value] inside"put a value into text. A value is anything you could write after 'set x to': a name, math, count of, query. Write \[ for a real bracket
add VALUE to LISTput a value at the end of a list
A and Bboth must be yes
ask database "SQL" with VALUE, ...run a query; each ? in the SQL takes the next value safely. Gives a list of records
show code TEXTshow Common code highlighted, the way a cl-code block does, but from a value
A contains Byes if text A has B inside it, or list A has item B
count of LISThow many items a list has, or letters in text
date of TEXTa date like 2026-10-24 written out: 24 October 2026
day of TEXTthe day of a date as a number
dictionaryevery word of the language as records: word, form, meaning, example
TEXT is an emailyes if the text is one email address, like ada@example.com. Check a visitor's address with it before you use it
endclose an if, for each, repeat, to, send email, record or text block
first of LIST / first N of LISTthe first item, or the first N items (or letters of text)
for each NAME in LISTrun the lines below once for every item
forget "name" / forget everythingdrop a remembered value, or all of them (sign out)
form "field"what the visitor typed in a submitted form (POST)
give VALUEinside a verb: hand this value back and finish. A verb that gives can be used wherever a value goes
go to "/page.html"send the visitor to another page and stop
hash of TEXTa one-way scramble of a password, safe to store (PBKDF2-HMAC-SHA256)
if CONDITIONrun the lines below when the condition is yes
include "file.html"insert another page here
A is Bequal. Also: is not, is more than, is less than, is at least, is at most, is between A and B, is empty
item N of LISTone item by position, counting from 1; or a column by name: item "price" of lamp
last of LIST / last N of LISTthe last item, or the last N items (or letters of text)
empty lista list with nothing in it yet
lowercase of TEXTthe text in small letters
TEXT matches HASHyes if the text is the password behind the hash
month of TEXTthe month of a date as a name
nothe answer no
not Aflip yes and no
nowthe date and time, like 2026-10-24 18:30
number of TEXTthe text as a number, so math works on it
COLUMN of RECORDone column of a record, like name of recipe. Any column name works
open database "file.db"open a SQLite file next to the page, or a Postgres server with postgres://user:password@host/dbname
A or Beither can be yes
otherwise [if CONDITION]what to run when the condition above was no
paragraphs of TEXTa list of the paragraphs (separated by blank lines)
paththe URL path, like /about. The visitor typed it, so it is made safe like query
query "name"what the visitor typed in the URL after ?name=
show raw VALUEshow text exactly as it is, with no safety
set NAME to record then column: VALUE lines, then enda record of your own, like a row you made up. Read a column with 'price of lamp', change one with 'set price of lamp to 15'
remember "name" as VALUEkeep a value for this visitor until they forget it (a signed cookie)
remembered "name"a value kept for this visitor, or empty text
repeat NUMBER times [until CONDITION]run the lines below that many times, or fewer with 'until CONDITION': stop as soon as it is yes
replace "a" with "b" in TEXTthe text with every a changed to b
reverse of LISTthe list back to front
send email to "you@site.com" then subject:, body:, reply to:, from: lines, then endsend an email to an address you wrote yourself (never a visitor's). While developing it is saved in the private outbox/ folder; behind a web server it goes through sendmail. At most 3 per page and 20 an hour
set NAME to VALUEcreate or replace a variable; VALUE, VALUE makes a list. 'set COLUMN of RECORD to VALUE' changes one column
show VALUE / show ... endshow a value on the page (visitor text is made safe). On its own, show opens a block of text that runs until a line saying end
sort of LISTthe list in order (numbers by size, text A to Z)
split TEXT by "/"a list of the pieces between each separator
tell database "SQL" with VALUE, ...run SQL that changes the database (insert, update, delete)
to NAME ARG ... then lines, then enddefine a verb: your own command. Call it by name with one value per ARG. Inside, 'give VALUE' hands a value back
todaythe date, like 2026-10-24
trim of TEXTthe text without spaces at the ends
try TEXTrun text as Common in a sandbox and give back what it said. No files, database, cookies or redirects; loops stop after 20000 steps and memory at 64 MB. For practice pages
uppercase of TEXTthe text in capitals
year of TEXTthe year of a date as a number
yesthe answer yes

Words that cannot be variable names: add and as ask at by code contains database dictionary each empty end everything for forget form go if in include is least less list matches more most no not now of open or otherwise path query raw remember remembered repeat show set tell than times to today try with yes give record until. Words that may also be names, because they only mean the Common word before of: count date day first hash item last lowercase month number paragraphs replace reverse sort split trim uppercase year. Kept for the future: page text.

17. Not in the dictionary

Habits from other languages, and the Common form. Each of the left-hand ones is an error that points to the right-hand one.

print, echo, sayshow
let x = 5, x = 5, varset x to 5
"Hello " + name, "Hello $name", "Hello {name}""Hello [name]"
==, equalsis
>, is bigger than, is greater thanis more than
else, elif, else ifotherwise, otherwise if
while, loop, foreachrepeat N times until …, for each
length, size, lencount of
session, cookie, login, logoutremember, remembered, forget everything
bcrypt, md5, password_verifyhash of, matches
substr, slice, sorted, includesfirst N of, sort of, contains
push, appendadd VALUE to LIST
upper, lower, stripuppercase of, lowercase of, trim of
int(), parseIntnumber of
$_POST, request.formform "field"
redirect, header("Location")go to
true, false, nullyes, no, empty
{ }, ;, 'single quotes'end, a new line, "double quotes"