Skip to content

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:

  1. Defines which properties can be referenced (via the Resolvable class)
  2. Accepts union types for properties that might receive references
  3. 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.