This tutorial uses AWS S3 because a bucket is the simplest resource to reason
about. Every structure you learn here (stack, target, resources, references,
properties) is identical on Azure, GCP, and every other cloud. Only the
resource types change.
Before you start
- formae installed and on your
PATH - The agent running in another terminal (
formae agent start) - AWS credentials in your environment
1
Create a project
A forma project is a directory with the Pkl dependencies it needs. Scaffold
one with the AWS schema included:This creates three files:
2
Write a minimal forma
Open Four things are happening here:
main.pkl and replace its contents with this:extends "@formae/forma.pkl"tells Pkl this file is a forma. This is whatformae project initgenerates; it also lets you add typed properties later. (The olderamends "@formae/forma.pkl"form still works.)- The
formablock holds everything formae will manage. formae.Stackgroups your resources. Destroying the stack destroys everything in it, so a stack is what you create and tear down as a whole, not just a folder for organizing files.formae.Targetsays where resources go: which cloud account and region.
label, which is how you refer to it inside
formae. It is not the name of the thing in your cloud. bucketName is the
real S3 name.3
Preview before you deploy
Never guess at what a forma will do. Run a dry run with This shows exactly what would be created, changed, or destroyed without
touching your cloud. If a Pkl expression is wrong, you find out here rather
than halfway through a real apply.
--simulate:4
Apply it
--mode reconcile means “make my cloud match this file exactly”. On a
terminal, apply shows a live progress view until it finishes; press q to
detach at any time and it keeps running on the agent.Confirm formae is managing the bucket:5
Reference one resource from another
Real infrastructure is connected: a subnet needs its VPC, an object needs its
bucket. formae handles this with The bucket is untouched (nothing about it changed) and the object is created.
.res.Add an object to your bucket. Note the two changes: the bucket becomes a
local you can refer to, and the object reads the bucket’s name through
.res.myBucket.res.bucketName is a resolvable: at write time the bucket does
not exist yet, so there is no name to hand the object. formae works out that
the object depends on the bucket, creates the bucket first, then fills in the
real value. You never declare dependencies or ordering yourself.Dry-run, then apply:6
Make it configurable
Hardcoding the bucket name means editing code for every environment.
Properties turn values into command-line flags.Declare a typed Two rules to remember:To see every property a forma exposes:
Props class (this is why the file extends rather than
amends), and use its members in the bucket:- The member name is the flag:
namebecomes--name,environmentbecomes--environment. - Read a property directly by its member name.
properties.nameis the string, no.valueto unwrap, and your editor resolves it.
CHANGE-ME default:7
Clean up
What you learned
- A forma is a stack (lifecycle group), a target (where), and resources (what).
labelidentifies a resource to formae; the cloud name is a separate field..resreferences another resource and gives you dependency ordering for free.- A
localmust still be mentioned insideformato be created. - Properties are members of a typed
Propsclass: each becomes a CLI flag, read directly by name (properties.name). apply --simulateshows you the truth before you change anything.
What’s next
Modular infrastructure
Split formae into reusable classes and functions as your infrastructure grows.
Apply modes
When to use reconcile and when to use patch.
Core concepts
Stacks, targets, resources, resolvables, and how formae stays in sync.
Pkl cheatsheet
The Pkl syntax you will actually use in formae.

