Resolvables
Resolvables enable resources to reference properties of other resources that aren't known until apply time. For example, a Subnet can reference a VPC's ID before the VPC exists—formae resolves the actual value when the VPC is created.
When to Use Resolvables
Use resolvables when a resource property depends on output from another resource:
- Subnet needs VPC ID (generated when VPC is created)
- Security Group references VPC ID
- IAM Policy references Role ARN
- Any property that uses an identifier generated by cloud infrastructure
Defining a Resolvable Class
Each resource type that can be referenced needs a Resolvable class:
import "@formae/formae.pkl"
const type = "MyPlugin::Network::VPC"
// Define what properties can be referenced
open class VPCResolvable extends formae.Resolvable {
hidden type = module.type
// Each resolvable property creates a self-referencing instance
hidden id: VPCResolvable = (this) {
property = "VpcId"
}
}
@formae.ResourceHint {
type = module.type
identifier = "VpcId"
}
open class VPC extends formae.Resource {
@formae.FieldHint {}
cidrBlock: String
local parent = this
// Expose resolvable for other resources to reference
hidden res: VPCResolvable = new {
label = parent.label
stack = parent.stack?.label
}
}
The hidden res property lets other resources reference this resource's properties.
Accepting Resolvable References
Use a union type to accept either a literal value or a resolvable reference:
@formae.ResourceHint {
type = "MyPlugin::Network::Subnet"
identifier = "SubnetId"
}
open class Subnet extends formae.Resource {
// Accepts either "vpc-123" or a reference to another resource
@formae.FieldHint {}
vpcId: String|formae.Resolvable
@formae.FieldHint {}
cidrBlock: String
}
Using Resolvables in Forma Files
Users reference resolvables through the resource's res property:
import "@myPlugin/network/vpc.pkl"
import "@myPlugin/network/subnet.pkl"
local myVpc = new vpc.VPC {
label = "main-vpc"
cidrBlock = "10.0.0.0/16"
}
new subnet.Subnet {
label = "public-subnet"
vpcId = myVpc.res.id // Reference the VPC's ID
cidrBlock = "10.0.1.0/24"
}
When evaluated, the vpcId property contains a resolvable marker that formae resolves at apply time.
How Resolution Works
formae uses resolvables to build a dependency graph that determines the order of operations. If a Subnet references a VPC's ID, formae knows to create the VPC first and destroy the Subnet first.
During apply, formae creates the VPC, waits for the response containing {"VpcId": "vpc-12345"}, then passes that resolved value to the Subnet's Create call. Your plugin never sees resolvable markers—just the final {"VpcId": "vpc-12345", "CidrBlock": "10.0.1.0/24"}.
Resources without dependencies on each other can be processed in parallel.
Note: Parent-child relationships (covered separately) are about discovery ordering. When formae discovers child resources, it automatically injects resolvables that feed into this same dependency graph.
Multiple Resolvable Properties
A resource can expose multiple resolvable properties:
open class BucketResolvable extends formae.Resolvable {
hidden type = "MyPlugin::Storage::Bucket"
hidden arn: BucketResolvable = (this) {
property = "Arn"
}
hidden name: BucketResolvable = (this) {
property = "BucketName"
}
hidden domainName: BucketResolvable = (this) {
property = "DomainName"
}
}
Users can then reference any of these:
new policy.Policy {
resource = myBucket.res.arn // "arn:aws:s3:::my-bucket"
bucketName = myBucket.res.name // "my-bucket"
}
Resolvables in Collections
Resolvables work within collections:
@formae.ResourceHint {
type = "MyPlugin::Network::SecurityGroup"
identifier = "SecurityGroupId"
}
open class SecurityGroup extends formae.Resource {
@formae.FieldHint {}
vpcId: String|formae.Resolvable
@formae.FieldHint {
updateMethod = "EntitySet"
indexField = "SecurityGroupId"
}
ingressRules: Listing<IngressRule>?
}
open class IngressRule {
// Can reference another security group
sourceSecurityGroupId: String|formae.Resolvable
port: Int
}
What Plugin Authors Need to Know
You don't handle resolution. formae's resolver handles all resolution logic. Your plugin:
- Defines which properties can be referenced (via the Resolvable class)
- Accepts union types for properties that might receive references
- Returns property values in Read/Create responses that formae uses to resolve references
The resolution machinery is entirely in formae's core—plugins just define the schema and implement CRUD operations.
Summary
| Concept | Purpose |
|---|---|
XxxResolvable class |
Declares which properties can be referenced |
String\|formae.Resolvable |
Accepts either literal or reference |
hidden res property |
Exposes resolvable for users |
Start by identifying which resource properties other resources might need to reference. Those are your resolvable candidates.