Resolvable

A resolvable is how you reference properties from other resources or access secrets — you read one off a resource through its .res accessor. It serves two main purposes:

  1. Reference properties of other infrastructure resources
  2. Access secrets securely, keeping sensitive values protected throughout the process

Resource Property Resolution

Infrastructure resources often depend on each other. For example, a subnet needs to know the VPC ID, or a database instance needs subnet IDs. Resolvables let you reference these values declaratively—you don't need to worry about creating resources in the right order.

formae automatically handles:

  • Detecting when resource properties are available
  • Waiting for properties that are still being created
  • Injecting resolved values at the right time

This eliminates common infrastructure management problems like explicit dependency declaration, manual ordering, and race conditions.

Note: The examples below use the local pattern to create variables for resources that need to be referenced later. This allows you to use .res to access their properties. Resources that don't need to be referenced can be created directly without local. Learn more about this pattern in formae 101 - Fundamentals.

Examples

Referencing VPC and Subnet IDs:

local vpc = new vpc.VPC {
  label = "main-vpc"
  cidrBlock = "10.0.0.0/16"
}
vpc

local subnet1 = new subnet.Subnet {
  label = "subnet-1"
  vpcId = vpc.res.vpcId  // Reference the VPC's ID using .res
  cidrBlock = "10.0.1.0/24"
  availabilityZone = "us-west-2a"
}
subnet1

new dbsubnetgroup.DBSubnetGroup {
  label = "db-subnet-group"
  dbSubnetGroupDescription = "Subnet group for RDS"
  subnetIds {
    subnet1.res.subnetId  // Reference subnet's ID
  }
}

Referencing secrets:

You can create secrets and reference them securely using resolvables. This keeps sensitive values protected while allowing you to use them in resource configurations.

// Create a secret with a randomly generated password
local dbSecret = new secret.Secret {
  label = "db-password"
  name = "my-db-password"
  description = "Database password secret"
  secretString = formae.value(random.password(12, false)).opaque.setOnce
}
dbSecret

// Reference the secret in a database instance
new dbinstance.DBInstance {
  label = "my-database"
  allocatedStorage = 20
  dbInstanceClass = "db.t3.micro"
  engine = "postgres"
  masterUsername = "admin"
  masterUserPassword = dbSecret.res.secretString  // Reference the secret using .res
}

In this example: - formae.value() wraps the password - .opaque marks it as a secret that should never be displayed - .setOnce ensures the value is generated once and reused - dbSecret.res.secretString references the secret's value in the database configuration

Note: Learn more about .opaque and .setOnce in the Values concept page.

Embedding resolvables in strings

The examples above use a resolvable as a field's entire value. Sometimes you instead need to splice a referenced value into the middle of a larger string — for example, a value that only exists after another resource is created, but which has to appear inside a block of code or a configuration template.

formae.embed(...) does this. Write the surrounding text as a string and interpolate any resolvable with \(...); formae resolves each reference at apply time and substitutes the real value into the text. Both resources still apply in a single pass — the referenced resource is ordered first automatically.

A CloudFront Function whose JavaScript needs the generated Id of a Key Value Store is the canonical case. Without embedding you would apply the store, copy its Id by hand, and re-apply the function; with it, one apply does both:

import "@aws/cloudfront/cffunction.pkl" as cffn
import "@aws/cloudfront/keyvaluestore.pkl" as kvsmod

// A Key Value Store — its Id is generated by AWS at create time, so it is not
// known until after the store exists.
local store = new kvsmod.KeyValueStore {
  label = "feature-flags"
  name = "feature-flags"
}
store

new cffn.Function {
  label = "rewrite"
  name = "rewrite"
  autoPublish = true
  functionConfig = new cffn.FunctionConfig {
    runtime = "cloudfront-js-2.0"
    comment = "Reads from the key value store"
    keyValueStoreAssociations = new Listing {
      new cffn.KeyValueStoreAssociation {
        keyValueStoreARN = store.res.arn
      }
    }
  }
  // The store's Id is embedded directly in the function source.
  functionCode = formae.embed("""
    import cf from 'cloudfront';
    const kvsId = '\(store.res.id)';
    async function handler(event) {
      const kv = cf.kvs(kvsId);
      return event.request;
    }
    """)
}

At apply time \(store.res.id) becomes the real Key Value Store Id (for example a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d), and the function is created with that value already in its source.

A few things to know:

  • A field has to opt in to accept embeds. The plugin's schema decides whether a field can be embedded (see Resolvables in the Plugin SDK). If a field rejects formae.embed(...), it does not support embedding — reference the whole value with .res instead.
  • formae extract round-trips embeds. Extracting a managed resource regenerates the formae.embed("…\(…)…") call rather than the resolved value, so re-applying is a no-op and the reference is never flattened to a literal.
  • You can embed more than one reference in the same string, and mix them with formae.value(...) secrets exactly as in any other field.

Target Config Resolution

Resolvables aren't limited to resource properties — they can also appear in target configurations. This enables cross-plugin patterns where one plugin provides infrastructure and another plugin's target resolves its connection details from it:

// Compose stack exposes endpoints as a Mapping
local lgtmStack = new compose.Stack {
  label = "lgtm"
  projectName = "formae-observability"
  composeFile = "..."
}

// Grafana target resolves a specific endpoint using at()
new formae.Target {
  label = "grafana"
  namespace = "GRAFANA"
  config = new grafana.Config {
    url = lgtmStack.res.endpoints.at("lgtm:3000")
  }
}

The at() method indexes into the endpoints Mapping by key — formae resolves it to the actual URL (e.g., http://localhost:3000) at apply time.

See Target resolvables for the full pattern and examples.

Collection Resolvables

When a resource property resolves to a collection (Mapping or Listing), use at() to reference individual items:

// Map key access — Docker Compose endpoints
url = composeStack.res.endpoints.at("grafana:3000")

// List index + field access — OVH database endpoints
uri = dbService.res.endpoints.at(0).uri

See Collection Resolvables in the plugin SDK docs for details on defining these in your schemas.