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
}

Collection Resolvables

Some resource properties resolve to collections (Mappings or Listings) rather than scalar values. For example, a Docker Compose stack exposes an endpoints Mapping, and an OVH database service exposes an endpoints Listing.

To let users reference individual items within these collections, use MappingResolvable or ListingResolvable instead of the base Resolvable:

Mapping Properties

Use MappingResolvable when a property resolves to a key-value map:

import "@formae/formae.pkl"

open class StackResolvable extends formae.Resolvable {
    local _self = this
    hidden endpoints: formae.MappingResolvable = new formae.MappingResolvable {
        label = _self.label
        type = _self.type
        stack = _self.stack
        property = "endpoints"
    }
}

Users access individual values with at(key):

// Resolves to "http://localhost:3000"
url = lgtmStack.res.endpoints.at("lgtm:3000")

Special characters in keys (colons, dots) are escaped automatically.

Listing Properties

Use ListingResolvable when a property resolves to an ordered list:

open class ServiceResolvable extends formae.Resolvable {
    local _self = this
    hidden endpoints: formae.ListingResolvable = new formae.ListingResolvable {
        label = _self.label
        type = _self.type
        stack = _self.stack
        property = "endpoints"
    }
}

Users access elements with at(index), which returns a MappingResolvable for further field navigation:

// Access the first endpoint's URI
uri = dbService.res.endpoints.at(0).at("uri")

Typed Element Resolvables

For list properties whose elements have a known structure, you can create a typed resolvable for the element. This gives users field-level access instead of the generic at(key):

/// Typed resolvable for endpoint objects in the list
open class EndpointResolvable extends formae.Resolvable {
    local _self = this
    hidden uri: formae.Resolvable = new formae.Resolvable {
        label = _self.label; type = _self.type; stack = _self.stack
        property = if (_self.property != null) _self.property + ".uri" else "uri"
    }
    hidden component: formae.Resolvable = new formae.Resolvable {
        label = _self.label; type = _self.type; stack = _self.stack
        property = if (_self.property != null) _self.property + ".component" else "component"
    }
}

/// Override at() to return the typed element
open class EndpointListingResolvable extends formae.ListingResolvable {
    local _self = this
    function at(index: Int): EndpointResolvable = new EndpointResolvable {
        label = _self.label; type = _self.type; stack = _self.stack
        property = if (_self.property != null)
            _self.property + "." + index.toString()
            else index.toString()
    }
}

Users get ergonomic field access:

// With typed element resolvable
uri = dbService.res.endpoints.at(0).uri

// Without (generic fallback)
uri = dbService.res.endpoints.at(0).at("uri")

This pattern is opt-in — the generic MappingResolvable.at(key) fallback always works. Use typed element resolvables when your users frequently reference specific fields in list items.

Return Type Guidelines

Use the correct resolvable type for each property to get compile-time safety:

Property resolves to Resolvable type at() available?
Scalar (String, Int, etc.) formae.Resolvable No (compile error)
Mapping formae.MappingResolvable at(key: String)
Listing formae.ListingResolvable at(index: 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.