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:
- Reference properties of other infrastructure resources
- 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
localpattern to create variables for resources that need to be referenced later. This allows you to use.resto access their properties. Resources that don't need to be referenced can be created directly withoutlocal. 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
.opaqueand.setOncein 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.resinstead. formae extractround-trips embeds. Extracting a managed resource regenerates theformae.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.