> ## Documentation Index
> Fetch the complete documentation index at: https://docs.formae.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Parent-child resources

> Declare parent-child relationships so formae can discover resources in the right order and inject references automatically.

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](/plugin-development/concepts/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`:

```kotlin theme={"languages":{"custom":["/languages/pkl.json"]}}
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:

```kotlin theme={"languages":{"custom":["/languages/pkl.json"]}}
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:

```json theme={"languages":{"custom":["/languages/pkl.json"]}}
{
  "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:

```kotlin theme={"languages":{"custom":["/languages/pkl.json"]}}
// 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 to Subnet to NetworkInterface (parents before children)

Update ordering (via injected resolvables):

* Apply: VPC to Subnet to NetworkInterface
* Destroy: NetworkInterface to Subnet to VPC

## Multiple List Parameters

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

```kotlin theme={"languages":{"custom":["/languages/pkl.json"]}}
@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`:

```go theme={"languages":{"custom":["/languages/pkl.json"]}}
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:

```kotlin theme={"languages":{"custom":["/languages/pkl.json"]}}
// 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):

```kotlin theme={"languages":{"custom":["/languages/pkl.json"]}}
@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.
