Skip to content

Parent-Child Resources

Some resources can only exist within the context of a parent resource. For example, an IAM RolePolicy belongs to an IAM Role, or an ECS TaskSet belongs to a Service. formae models these relationships to enable tiered discovery and correct ordering of create/destroy operations.

Why Parent-Child Matters

Parent-child relationships exist primarily for discovery. To list child resources, you need the parent's identifier. formae discovers parents first, then uses their properties to discover children.

When formae discovers a child resource, it automatically injects a resolvable reference from the child to its parent. This resolvable reference feeds into formae's dependency DAG, which ensures correct ordering during apply and destroy operations. See Resolvables for details on how the DAG determines update ordering.

formae handles discovery ordering and resolvable injection automatically when you declare the relationship correctly.

Declaring a Parent-Child Relationship

Use the parent and listParam fields in your @ResourceHint:

import "@formae/formae.pkl"
import "../myplugin.pkl"

const type = "MyPlugin::IAM::RolePolicy"

@myplugin.ResourceHint {
    type = module.type
    identifier = "PolicyName"

    // This resource's parent
    parent = "MyPlugin::IAM::Role"

    // How to query children from the parent
    listParam = new formae.ListProperty {
        parentProperty = "RoleName"    // Property in the parent resource
        listParameter = "RoleName"     // Parameter name your List API expects
    }
}
open class RolePolicy extends formae.Resource {
    @myplugin.FieldHint {}
    policyName: String

    // Accept either a string or a reference to the parent
    @myplugin.FieldHint { createOnly = true }
    roleName: String|formae.Resolvable
}

The ListProperty Class

formae.ListProperty maps parent properties to List API parameters:

open class ListProperty {
    // Name of the property in the parent resource that contains the value
    hidden parentProperty: String

    // Name of the parameter your plugin's List operation expects
    hidden listParameter: String
}

Example: For IAM RolePolicies: - parentProperty = "RoleName" → Get the RoleName from the discovered Role - listParameter = "RoleName" → Pass it to the List API as the RoleName parameter

How Tiered Discovery Works

When formae runs discovery:

  1. Discover root resources: Resources without a parent are discovered first (VPCs, Roles, S3 Buckets)

  2. Extract parent properties: For each discovered parent, formae extracts the parentProperty value

  3. Discover children: formae calls your plugin's List() operation with the parent's property value in AdditionalProperties

  4. Recurse: If children have their own children, the process continues

VPC (discovered)
  └─ VpcId: "vpc-123"
       │
       ▼ List(AdditionalProperties: {"VpcId": "vpc-123"})
       │
Subnet (discovered)
  └─ SubnetId: "subnet-456"
       │
       ▼ List(AdditionalProperties: {"SubnetId": "subnet-456"})
       │
NetworkInterface (discovered)

Automatic Resolvable Injection

When formae discovers a child resource, it automatically creates a resolvable reference to the parent:

{
  "SubnetId": "subnet-456",
  "VpcId": {
    "$ref": "formae://2ABC123#/VpcId",
    "$value": "vpc-123"
  }
}

This resolvable reference is added to formae's dependency DAG, which determines update ordering: - Apply: VPC is created before Subnet (dependency first) - Destroy: Subnet is destroyed before VPC (dependent first)

The ordering mechanism is the same as any other resolvable—parent-child just automates the injection during discovery.

Multi-Level Hierarchies

Parent-child relationships can be nested to any depth:

// VPC - root resource (no parent)
@aws.ResourceHint {
    type = "AWS::EC2::VPC"
    identifier = "VpcId"
}

// Subnet - child of VPC
@aws.ResourceHint {
    type = "AWS::EC2::Subnet"
    identifier = "SubnetId"
    parent = "AWS::EC2::VPC"
    listParam = new formae.ListProperty {
        parentProperty = "VpcId"
        listParameter = "VpcId"
    }
}

// NetworkInterface - child of Subnet
@aws.ResourceHint {
    type = "AWS::EC2::NetworkInterface"
    identifier = "NetworkInterfaceId"
    parent = "AWS::EC2::Subnet"
    listParam = new formae.ListProperty {
        parentProperty = "SubnetId"
        listParameter = "SubnetId"
    }
}

Discovery order: VPC → Subnet → NetworkInterface (parents before children)

Update ordering (via injected resolvables): - Apply: VPC → Subnet → NetworkInterface - Destroy: NetworkInterface → Subnet → VPC

Multiple List Parameters

Some resources require multiple parent properties to list. Use a list of ListProperty:

@myplugin.ResourceHint {
    type = "MyPlugin::ECS::TaskSet"
    identifier = "Id"
    parent = "MyPlugin::ECS::Service"
    listParam = new Listing {
        new formae.ListProperty {
            parentProperty = "ServiceName"
            listParameter = "Service"
        }
        new formae.ListProperty {
            parentProperty = "Cluster"
            listParameter = "Cluster"
        }
    }
}

Your plugin's List() receives both parameters in AdditionalProperties.

Plugin Implementation

Your plugin's List() operation receives parent properties in request.AdditionalProperties:

func (p *Plugin) List(ctx context.Context, req *resource.ListRequest) (*resource.ListResult, error) {
    // For child resources, AdditionalProperties contains parent values
    // e.g., {"RoleName": "my-role"} for listing RolePolicies

    if len(req.AdditionalProperties) > 0 {
        // Use these to filter your API call
        roleName := req.AdditionalProperties["RoleName"]
        // Call your backend API with this filter
    }

    // Return discovered resources
}

When to Use Parent-Child

Use parent-child relationships when:

Scenario Example
API requires parent ID to list IAM RolePolicies need RoleName
Resources are scoped to a parent Subnets exist within a VPC
Destroying parent should destroy children Deleting a Role deletes its policies

Don't use parent-child for: - Simple references (Subnet references VPC but isn't "inside" it conceptually) - Resources that can exist independently - Resources where you don't need tiered discovery

Common Patterns

AWS CloudControl Resources

Many AWS resources follow the parent-child pattern:

// EFS Mount Target (child of FileSystem)
@aws.ResourceHint {
    type = "AWS::EFS::MountTarget"
    identifier = "MountTargetId"
    parent = "AWS::EFS::FileSystem"
    listParam = new formae.ListProperty {
        parentProperty = "FileSystemId"
        listParameter = "FileSystemId"
    }
}

Resources Without Discovery

Some child resources can't be discovered (API doesn't support listing):

@aws.ResourceHint {
    type = "AWS::EC2::Route"
    identifier = "Ref"
    discoverable = false  // No List API available
}

These resources must be defined in Forma files; they won't be auto-discovered.

Summary

Field Purpose
parent Parent resource type (e.g., "AWS::IAM::Role")
listParam.parentProperty Which property to read from parent
listParam.listParameter What to call it in List request

Declare parent-child relationships to enable tiered discovery and automatic ordering. formae handles the complexity of discovering in the right order and injecting references automatically.