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
theis silent, soset total to the count of fruits.reads as a sentence and meansset total to count of fruits. - Words are lowercase.
Sayis an error that tells you to writeshow. - 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.
| Kind | Looks like | Notes |
|---|---|---|
| Text | "Hello" | Always in double quotes. May contain brackets. |
| Number | 42, 4.5, -3 | Whole or decimal. Shown without trailing zeros: 4.50 prints as 4.5. |
| Answer | yes, no | What a condition gives. Prints as the word. |
| List | "a", "b", "c" | Items in order, counted from 1. Prints as a, b, c. |
| Record | one row from the database | Named 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 b | equal | a is not b | not equal |
a is more than b | greater | a is less than b | smaller |
a is at least b | greater or equal | a is at most b | smaller or equal |
a is between 1 and 10 | from 1 to 10, inclusive | a is not between 1 and 10 | outside that range |
a is empty | empty text, empty list or zero | a is not empty | has 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 VALUEhands a value back and finishes the verb. A verb that gives can be used wherever a value goes: in brackets, afterset … 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 x | HELLO |
lowercase of x | hello |
trim of x | no spaces at the ends |
split x by "/" | a list of the pieces; empty pieces are skipped |
replace "a" with "b" in x | every a becomes b |
paragraphs of x | a list, split on blank lines, each trimmed |
number of x | the text as a number, or an error if it is not one |
count of x | how many letters |
first 100 of x, last 3 of x | the first or last N letters (or items of a list) |
x contains "@" | yes if the piece is inside |
sort of list, reverse of list | in order, or back to front |
date of x, year of x, month of x, day of x | 24 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. |
path | the address after the site name: /blog/hello-world |
today | 2026-10-24 |
now | 2026-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.
11. Pages and addresses
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.
/aboutservesabout.html./blog/hello-worldservesblog.html, and the page reads the rest withlast of split path by "/". - A custom not-found page. Put a
404.htmlin 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.secretand 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, b | a query; gives a list of records |
tell database "SQL" with a, b | insert, update, delete, create |
column of record | one 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 wrote | Common 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 = 5 | Common has no '='. Write 'set NAME to VALUE' to store, or 'is' to compare |
if a > 2 | Common 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 5 | expected 'to'. The whole line is written: set NAME to VALUE |
show totl | I do not know what 'totl' is. Did you mean 'total'? |
a missing end | this 'if' is never closed. Add a line with just 'end' after the lines it decides about |
"ten" * 2 | can 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.
| Form | Meaning |
|---|---|
"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 LIST | put a value at the end of a list |
A and B | both 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 TEXT | show Common code highlighted, the way a cl-code block does, but from a value |
A contains B | yes if text A has B inside it, or list A has item B |
count of LIST | how many items a list has, or letters in text |
date of TEXT | a date like 2026-10-24 written out: 24 October 2026 |
day of TEXT | the day of a date as a number |
dictionary | every word of the language as records: word, form, meaning, example |
TEXT is an email | yes if the text is one email address, like ada@example.com. Check a visitor's address with it before you use it |
end | close an if, for each, repeat, to, send email, record or text block |
first of LIST / first N of LIST | the first item, or the first N items (or letters of text) |
for each NAME in LIST | run the lines below once for every item |
forget "name" / forget everything | drop a remembered value, or all of them (sign out) |
form "field" | what the visitor typed in a submitted form (POST) |
give VALUE | inside 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 TEXT | a one-way scramble of a password, safe to store (PBKDF2-HMAC-SHA256) |
if CONDITION | run the lines below when the condition is yes |
include "file.html" | insert another page here |
A is B | equal. 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 LIST | one item by position, counting from 1; or a column by name: item "price" of lamp |
last of LIST / last N of LIST | the last item, or the last N items (or letters of text) |
empty list | a list with nothing in it yet |
lowercase of TEXT | the text in small letters |
TEXT matches HASH | yes if the text is the password behind the hash |
month of TEXT | the month of a date as a name |
no | the answer no |
not A | flip yes and no |
now | the date and time, like 2026-10-24 18:30 |
number of TEXT | the text as a number, so math works on it |
COLUMN of RECORD | one 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 B | either can be yes |
otherwise [if CONDITION] | what to run when the condition above was no |
paragraphs of TEXT | a list of the paragraphs (separated by blank lines) |
path | the 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 VALUE | show text exactly as it is, with no safety |
set NAME to record then column: VALUE lines, then end | a 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 VALUE | keep 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 TEXT | the text with every a changed to b |
reverse of LIST | the list back to front |
send email to "you@site.com" then subject:, body:, reply to:, from: lines, then end | send 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 VALUE | create or replace a variable; VALUE, VALUE makes a list. 'set COLUMN of RECORD to VALUE' changes one column |
show VALUE / show ... end | show 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 LIST | the 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 end | define a verb: your own command. Call it by name with one value per ARG. Inside, 'give VALUE' hands a value back |
today | the date, like 2026-10-24 |
trim of TEXT | the text without spaces at the ends |
try TEXT | run 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 TEXT | the text in capitals |
year of TEXT | the year of a date as a number |
yes | the 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, say | show |
let x = 5, x = 5, var | set x to 5 |
"Hello " + name, "Hello $name", "Hello {name}" | "Hello [name]" |
==, equals | is |
>, is bigger than, is greater than | is more than |
else, elif, else if | otherwise, otherwise if |
while, loop, foreach | repeat N times until …, for each |
length, size, len | count of |
session, cookie, login, logout | remember, remembered, forget everything |
bcrypt, md5, password_verify | hash of, matches |
substr, slice, sorted, includes | first N of, sort of, contains |
push, append | add VALUE to LIST |
upper, lower, strip | uppercase of, lowercase of, trim of |
int(), parseInt | number of |
$_POST, request.form | form "field" |
redirect, header("Location") | go to |
true, false, null | yes, no, empty |
{ }, ;, 'single quotes' | end, a new line, "double quotes" |