> ## Documentation Index
> Fetch the complete documentation index at: https://docs.formae.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Write your first forma

> Build a forma from scratch: a stack, a target, resources, references, and properties.

In the [Quick start](/documentation/get-started/quickstart) you deployed an
example someone else wrote. Now you will write one yourself, from an empty
directory to running infrastructure.

You will create an S3 bucket, put a file in it that references the bucket, and
then make the whole thing configurable from the command line.

<Note>
  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.
</Note>

## Before you start

* formae installed and on your `PATH`
* The agent running in another terminal (`formae agent start`)
* AWS credentials in your environment

If any of that is missing, work through the [Quick start](/documentation/get-started/quickstart)
first.

<Steps>
  <Step title="Create a project">
    A forma project is a directory with the Pkl dependencies it needs. Scaffold
    one with the AWS schema included:

    ```bash theme={"languages":{"custom":["/languages/pkl.json"]}}
    mkdir -p ~/projects && cd ~/projects
    formae project init --include aws first-forma
    cd first-forma
    ```

    This creates three files:

    | File                   | What it does                                                           |
    | ---------------------- | ---------------------------------------------------------------------- |
    | `PklProject`           | Declares your package dependencies (the formae SDK and the AWS schema) |
    | `PklProject.deps.json` | The resolved dependency lock file                                      |
    | `main.pkl`             | A starter forma for you to replace                                     |
  </Step>

  <Step title="Write a minimal forma">
    Open `main.pkl` and replace its contents with this:

    ```pkl theme={"languages":{"custom":["/languages/pkl.json"]}}
    extends "@formae/forma.pkl"
    import "@formae/formae.pkl"

    import "@aws/aws.pkl"
    import "@aws/s3/bucket.pkl"

    forma {
      new formae.Stack {
        label = "first-forma"
        description = "My first formae stack"
      }

      new formae.Target {
        label = "aws-target"
        config = new aws.Config {
          region = "us-east-1"
        }
      }

      new bucket.Bucket {
        label = "my-first-bucket"
        bucketName = "formae-first-forma-CHANGE-ME"
      }
    }
    ```

    <Warning>
      S3 bucket names are globally unique across all of AWS. Replace `CHANGE-ME`
      with something nobody else will have used. You will fix this properly with a
      property in a later step.
    </Warning>

    Four things are happening here:

    * **`extends "@formae/forma.pkl"`** tells Pkl this file is a forma. This is
      what `formae project init` generates; it also lets you add typed
      [properties](/documentation/concepts/properties) later. (The older
      `amends "@formae/forma.pkl"` form still works.)
    * **The `forma` block** holds everything formae will manage.
    * **`formae.Stack`** groups 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.Target`** says *where* resources go: which cloud account and
      region.

    Every resource takes a **`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.
  </Step>

  <Step title="Preview before you deploy">
    Never guess at what a forma will do. Run a dry run with `--simulate`:

    ```bash theme={"languages":{"custom":["/languages/pkl.json"]}}
    formae apply --mode reconcile --simulate main.pkl
    ```

    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.
  </Step>

  <Step title="Apply it">
    ```bash theme={"languages":{"custom":["/languages/pkl.json"]}}
    formae apply --mode reconcile main.pkl
    ```

    `--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:

    ```bash theme={"languages":{"custom":["/languages/pkl.json"]}}
    formae inventory resources
    ```
  </Step>

  <Step title="Reference one resource from another">
    Real infrastructure is connected: a subnet needs its VPC, an object needs its
    bucket. formae handles this with `.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`.

    ```pkl theme={"languages":{"custom":["/languages/pkl.json"]}}
    extends "@formae/forma.pkl"
    import "@formae/formae.pkl"

    import "@aws/aws.pkl"
    import "@aws/s3/bucket.pkl"
    import "@aws/s3/object.pkl"

    local myBucket = new bucket.Bucket {
      label = "my-first-bucket"
      bucketName = "formae-first-forma-CHANGE-ME"
    }

    forma {
      new formae.Stack {
        label = "first-forma"
        description = "My first formae stack"
      }

      new formae.Target {
        label = "aws-target"
        config = new aws.Config {
          region = "us-east-1"
        }
      }

      myBucket

      new object.Object {
        label = "readme-object"
        bucket = myBucket.res.bucketName
        key = "hello.txt"
        content = "Written by my first forma."
        contentType = "text/plain"
      }
    }
    ```

    `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.

    <Warning>
      Declaring `local myBucket` is not enough to deploy it. You must also mention
      `myBucket` inside the `forma` block, as above. A `local` that is never
      referenced in `forma` is simply not rendered, and formae will not create it.
    </Warning>

    Dry-run, then apply:

    ```bash theme={"languages":{"custom":["/languages/pkl.json"]}}
    formae apply --mode reconcile --simulate main.pkl
    formae apply --mode reconcile main.pkl
    ```

    The bucket is untouched (nothing about it changed) and the object is created.
  </Step>

  <Step title="Make it configurable">
    Hardcoding the bucket name means editing code for every environment.
    Properties turn values into command-line flags.

    Declare a typed `Props` class (this is why the file `extends` rather than
    `amends`), and use its members in the bucket:

    ```pkl theme={"languages":{"custom":["/languages/pkl.json"]}}
    properties: Props

    class Props {
      /// Globally-unique S3 bucket name
      name: String = "formae-first-forma-CHANGE-ME"

      /// Deployment environment
      environment: String = "dev"
    }

    local myBucket = new bucket.Bucket {
      label = "my-first-bucket"
      bucketName = properties.name
      tags {
        new { key = "Environment"; value = properties.environment }
      }
    }
    ```

    Two rules to remember:

    * The member name is the flag: `name` becomes `--name`, `environment`
      becomes `--environment`.
    * Read a property directly by its member name. `properties.name` *is* the
      string, no `.value` to unwrap, and your editor resolves it.

    Now pass the properties on the command line, and the same forma serves any
    environment. This also gives the bucket a real name instead of the
    `CHANGE-ME` default:

    ```bash theme={"languages":{"custom":["/languages/pkl.json"]}}
    formae apply --mode reconcile --simulate --name my-unique-bucket-name --environment staging main.pkl
    formae apply --mode reconcile --watch --name my-unique-bucket-name --environment staging main.pkl
    ```

    To see every property a forma exposes:

    ```bash theme={"languages":{"custom":["/languages/pkl.json"]}}
    formae apply --help main.pkl
    ```
  </Step>

  <Step title="Clean up">
    ```bash theme={"languages":{"custom":["/languages/pkl.json"]}}
    formae destroy main.pkl
    ```

    Because everything lives in one stack, this removes the object and the bucket
    together, in the right order.
  </Step>
</Steps>

## What you learned

* A forma is a **stack** (lifecycle group), a **target** (where), and
  **resources** (what).
* **`label`** identifies a resource to formae; the cloud name is a separate
  field.
* **`.res`** references another resource and gives you dependency ordering for
  free.
* A **`local`** must still be mentioned inside `forma` to be created.
* **Properties** are members of a typed `Props` class: each becomes a CLI flag,
  read directly by name (`properties.name`).
* **`apply --simulate`** shows you the truth before you change anything.

## What's next

<CardGroup cols={2}>
  <Card title="Modular infrastructure" href="/documentation/guides/modular-infrastructure">
    Split formae into reusable classes and functions as your infrastructure grows.
  </Card>

  <Card title="Apply modes" href="/documentation/concepts/apply-modes">
    When to use reconcile and when to use patch.
  </Card>

  <Card title="Core concepts" href="/documentation/concepts/label">
    Stacks, targets, resources, resolvables, and how formae stays in sync.
  </Card>

  <Card title="Pkl cheatsheet" href="/documentation/reference/pkl-cheatsheet">
    The Pkl syntax you will actually use in formae.
  </Card>
</CardGroup>
