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

# PKL schema reference

> The full formae PKL schema a plugin author works with: resource and field annotations, relationship and ordering types, the value types you emit, and the schema module shape.

This page documents the complete formae PKL schema module (`@formae/formae.pkl`) as a plugin author uses it. It covers the annotations you apply to resource classes and fields, the types that describe relationships and update ordering, the value types your resources emit, and the overall shape of a schema module.

For a hands-on introduction, start with [02 - Resource schema](/plugin-development/tutorial/02-schema). For PKL language basics, see the [PKL cheatsheet](/documentation/reference/pkl-cheatsheet).

## How a schema is structured

A plugin's schema module amends nothing; it imports `@formae/formae.pkl` and defines resource classes. Each resource class extends `formae.Resource`, carries a `@formae.ResourceHint` annotation, and annotates each user-settable property with `@formae.FieldHint`:

```pkl theme={"languages":{"custom":["/languages/pkl.json"]}}
module mycloud

import "@formae/formae.pkl"

/// A compute instance in MyCloud.
@formae.ResourceHint {
    type = "MYCLOUD::Compute::Instance"
    identifier = "$.InstanceId"
}
class Instance extends formae.Resource {
    fixed hidden type: String = "MYCLOUD::Compute::Instance"

    @formae.FieldHint { required = true }
    name: String

    @formae.FieldHint { createOnly = true }
    imageId: String
}
```

The `Resource` base class supplies the shared attributes every managed resource carries: `label`, `group`, `target`, `stack`, and `alias` (a previous label used to rename a managed row in place). It also exposes reflection helpers (`fields()`, `hints()`, `schema()`, `type()`, `props()`) that formae calls at evaluation time to derive the resource descriptor from your annotations. You do not call these yourself.

***

## Resource annotations

### @formae.ResourceHint

Applied to a resource class to declare its type metadata. `type` and `identifier` are the only required fields; everything else has a default.

| Field          | Type                                    | Default  | Description                                                             |
| -------------- | --------------------------------------- | -------- | ----------------------------------------------------------------------- |
| `type`         | `String`                                | required | Full resource type, must match `NAMESPACE::SERVICE::RESOURCE`           |
| `identifier`   | `String`                                | required | Property (JSONPath) to store as the native ID after create              |
| `parent`       | `String?`                               | `null`   | Type of the parent resource, for nested resources                       |
| `parentRefs`   | `(ParentRef \| List<ParentRef>)?`       | `null`   | Maps parent identity properties to the child properties that carry them |
| `listParam`    | `(ListProperty \| List<ListProperty>)?` | `null`   | Parent property (or properties) required for List operations            |
| `discoverable` | `Boolean`                               | `true`   | When `false`, the resource is excluded from discovery                   |
| `extractable`  | `Boolean`                               | `true`   | When `false`, the resource is excluded from CLI extraction              |
| `portable`     | `Boolean`                               | `false`  | When `true`, the resource can be moved across targets                   |
| `docComment`   | `String?`                               | `null`   | Documentation comment carried through into the descriptor               |

There is also a `hidden outputKeyTransformation: (String) -> String` (identity by default). It lets a schema rewrite every PKL property name into the wire key your provider expects (for example, capitalizing keys). It is `hidden`, so you set it in a subclass rather than in the annotation body.

#### type

The full resource type, validated against the pattern `NAMESPACE::SERVICE::RESOURCE` (three segments separated by `::`):

```pkl theme={"languages":{"custom":["/languages/pkl.json"]}}
type = "AWS::EC2::Instance"
type = "MYCLOUD::Storage::Bucket"
```

* `NAMESPACE`: your plugin's namespace (uppercase), matching the `namespace` in your [plugin manifest](/plugin-development/reference/manifest).
* `SERVICE`: a logical service grouping, for example `EC2` or `Storage`.
* `RESOURCE`: the resource name in PascalCase.

#### identifier

The property that formae stores as the resource's native ID after create. It is a JSONPath expression evaluated against the properties your plugin returns:

```pkl theme={"languages":{"custom":["/languages/pkl.json"]}}
identifier = "$.InstanceId"   // top-level field
identifier = "$.Resource.Id"  // nested field
identifier = "$.Arn"          // ARN as identifier
```

The native ID must be unique within the type, stable across the resource's lifetime, and present in the output of your Create operation. formae passes it to your Read, Update, and Delete operations.

#### parent, parentRefs, and listParam

These three fields describe a child resource's relationship to its parent. See [Parent and child resources](/plugin-development/concepts/parent-child-resources) for the full model.

`parent` names the parent resource type:

```pkl theme={"languages":{"custom":["/languages/pkl.json"]}}
parent = "MYCLOUD::Storage::Bucket"
```

`parentRefs` is the explicit way to map a parent-side identity property to the child-side property that carries it. The two names need not match. Supply one `ParentRef` or a `List` of them:

```pkl theme={"languages":{"custom":["/languages/pkl.json"]}}
@formae.ResourceHint {
    type = "AWS::ECS::TaskSet"
    identifier = "$.Id"
    parent = "AWS::ECS::Service"
    parentRefs = new formae.ParentRef {
        parentProperty = "ServiceName"  // property on the parent
        childProperty = "Service"       // property on this child
    }
}
```

`listParam` is the legacy way to express the same relationship for List operations, using [`ListProperty`](#listproperty). When both are present, `parentRefs` wins; when only `listParam` is present, formae derives the parent mapping from it (the child-side name comes from `listParameter`). Prefer `parentRefs` for new schemas.

```pkl theme={"languages":{"custom":["/languages/pkl.json"]}}
listParam = new formae.ListProperty {
    parentProperty = "BucketName"  // property on the parent
    listParameter = "Bucket"        // parameter the List API expects
}
```

For resources that need multiple parent properties, pass a `List(...)` of either type.

#### discoverable, extractable, portable

Each is a boolean gate:

```pkl theme={"languages":{"custom":["/languages/pkl.json"]}}
@formae.ResourceHint {
    type = "MYCLOUD::Internal::TempResource"
    identifier = "$.Id"
    discoverable = false   // hidden from discovery
    extractable = false    // excluded from CLI extraction
    portable = true        // may be recreated on a different target
}
```

* `discoverable = false` keeps internal or high-volume resources out of discovery results.
* `extractable = false` keeps a resource out of CLI extraction output (useful for auto-generated or internal state).
* `portable = true` marks a resource as safe to recreate on a different target. When a target replace is triggered, formae proceeds only if every resource on the target is portable. Non-portable resources (the default) block a target replace so that region-bound data and region-specific configuration are never silently moved.

### @formae.SubResourceHint

Applied to a `formae.SubResource` class (a nested structured field). It carries only the `hidden outputKeyTransformation: (String) -> String` (identity by default), which rewrites nested property names on the wire the same way `ResourceHint.outputKeyTransformation` does for top-level properties. A sub-resource without this annotation renders its property names unchanged.

```pkl theme={"languages":{"custom":["/languages/pkl.json"]}}
class NetworkConfig extends formae.SubResource {
    @formae.FieldHint {}
    vpcId: String

    @formae.FieldHint {}
    subnetIds: Listing<String>
}
```

Field hints on a sub-resource's properties are propagated to the descriptor under a dotted key (for example `networkConfig.vpcId`), including through `Listing<SubResource>` fields and nested sub-resources. Self-referencing sub-resource types are handled without infinite recursion.

***

## Field annotations

### @formae.FieldHint

Applied to a resource or sub-resource property. Every user-settable property needs a `@formae.FieldHint`; formae only tracks annotated properties. An empty `@formae.FieldHint {}` marks a normal, mutable, in-place-updatable field.

| Field                 | Type                 | Default     | Description                                                                           |
| --------------------- | -------------------- | ----------- | ------------------------------------------------------------------------------------- |
| `createOnly`          | `Boolean`            | `false`     | Changing the field forces a replace (delete + create)                                 |
| `writeOnly`           | `Boolean`            | `false`     | Written but never read back; stripped before drift comparison                         |
| `required`            | `Boolean`            | `false`     | Must be provided on every create and update                                           |
| `requiredOnCreate`    | `Boolean`            | `false`     | Must be provided on create, optional on update                                        |
| `requiredOnUpdate`    | `Boolean`            | `false`     | Re-sent on every update even when unchanged                                           |
| `hasProviderDefault`  | `Boolean`            | `false`     | Provider assigns a default when the field is omitted                                  |
| `edgeKind`            | `EdgeKind`           | `"default"` | Dependency edge classification for ordering (see below)                               |
| `attachesTo`          | `Boolean`            | `false`     | Deprecated. Superseded by `edgeKind = "attachesTo"`                                   |
| `indexField`          | `String?`            | `null`      | Key field for `EntitySet` collections                                                 |
| `updateMethod`        | `FieldUpdateMethod?` | `null`      | Collection comparison strategy                                                        |
| `preserveEmptyValues` | `Boolean`            | `false`     | Empty objects and lists inside the field are meaningful values, never cleanup targets |
| `format`              | `String`             | `""`        | Serialized-content format so content is compared, not bytes                           |

There are also three output-shaping fields, all `hidden`, used when the value you render differs from what the user declares:

| Field                  | Type                 | Default       | Description                                              |
| ---------------------- | -------------------- | ------------- | -------------------------------------------------------- |
| `outputField`          | `String?`            | `null`        | Override the rendered key for this property              |
| `outputTransformation` | `((Any) -> Any)?`    | `null`        | Transform the value before it is rendered                |
| `outputCondition`      | `((Any) -> Boolean)` | `(_) -> true` | Render the transformed value only when this returns true |

<Note>
  `required` is also inferred from the property's type. A non-nullable property is treated as required even without `required = true`; a nullable property (`String?`) is optional unless you set `required = true`.
</Note>

#### createOnly

The field cannot be changed after create. Changing it forces a replace (delete + create):

```pkl theme={"languages":{"custom":["/languages/pkl.json"]}}
@formae.FieldHint { createOnly = true }
region: String
```

#### writeOnly

The field can be written but is never returned by Read. It is stored by formae, stripped from read-back state before comparison, and so never produces drift. Common for passwords and secrets:

```pkl theme={"languages":{"custom":["/languages/pkl.json"]}}
@formae.FieldHint { writeOnly = true }
password: String
```

`writeOnly` controls read-back only. It does not, on its own, re-send the value on every update. If the provider drops a write-only value unless it is present in each update request, pair it with `requiredOnUpdate`.

#### opaque

Marks the field as a secret so the agent stores it hashed at rest. You don't set `opaque` directly; formae derives it from the field type. Type the field `(String | formae.SecretValue)` to make it opaque. `opaque` is independent of `writeOnly`, which controls read-back and drift. See [SecretValue](#secretvalue).

#### required and requiredOnCreate

`required` fields are validated before the command reaches your plugin; a missing required field fails with a validation error. `requiredOnCreate` fields must be present on create but may be omitted on update:

```pkl theme={"languages":{"custom":["/languages/pkl.json"]}}
@formae.FieldHint { required = true }
name: String

@formae.FieldHint { requiredOnCreate = true }
initialPassword: String?
```

#### requiredOnUpdate

The provider drops the field unless it is re-sent on every update. formae force-resends the field in each update patch even when its value has not changed. Typical for password-class fields and write-only configuration blocks that a provider replaces wholesale:

```pkl theme={"languages":{"custom":["/languages/pkl.json"]}}
@formae.FieldHint { writeOnly = true; requiredOnUpdate = true }
clientSecret: String?
```

#### hasProviderDefault

The provider assigns a value when the user omits the field. formae then accepts that provider value rather than generating a "remove" operation, which prevents oscillation on every reconcile:

```pkl theme={"languages":{"custom":["/languages/pkl.json"]}}
@formae.FieldHint { hasProviderDefault = true }
bucketEncryption: BucketEncryption?
```

When the user omits the field, formae accepts the provider's default (no patch). When the user sets it, formae applies the user's value (a patch if it differs).

#### edgeKind and attachesTo

`edgeKind` classifies the dependency edge that a cross-resource reference in this field creates, which controls create and destroy ordering. Its allowed values come from the [`EdgeKind`](#edgekind-typealias) type alias. Set it on the field that references another resource:

```pkl theme={"languages":{"custom":["/languages/pkl.json"]}}
@formae.FieldHint { edgeKind = "attachesTo" }
targetGroupArn: (String|formae.Resolvable)?
```

`attachesTo` is the older boolean form of the same idea (`attachesTo = true` is equivalent to `edgeKind = "attachesTo"`). It is deprecated in favor of `edgeKind`; prefer `edgeKind` in new schemas.

#### updateMethod and indexField

`updateMethod` controls how a collection field is compared between desired and actual state. Its allowed values come from the [`FieldUpdateMethod`](#fieldupdatemethod-typealias) type alias. `indexField` names the key property for `EntitySet` collections. See [Collection semantics](/plugin-development/concepts/collection-semantics) for the full model.

```pkl theme={"languages":{"custom":["/languages/pkl.json"]}}
@formae.FieldHint { updateMethod = "EntitySet"; indexField = "Key" }
tags: Listing<Tag>?

@formae.FieldHint { updateMethod = "Atomic" }
policyDocument: Dynamic?
```

| Value         | Behavior                                               |
| ------------- | ------------------------------------------------------ |
| (none)        | Unordered set comparison                               |
| `"Array"`     | Position-based comparison                              |
| `"Set"`       | Explicit unordered set                                 |
| `"EntitySet"` | Key-based comparison, requires `indexField`            |
| `"Atomic"`    | Opaque value, single replace with no sub-field diffing |

#### preserveEmptyValues

By default formae treats empty objects and lists inside a property as rendering noise and cleans them out before diffing and before writing. For most typed fields that is the right call. For document-style fields whose meaning the provider owns, an empty member can be the configuration itself: a Kubernetes custom resource spec selects a variant by key presence, so `selfSigned: {}` is a complete, valid declaration that must reach the apiserver exactly as written.

Set `preserveEmptyValues = true` to make every empty object and list inside the field a value in its own right. The field's content is then preserved verbatim through diffing, change detection, and the payload handed to your plugin, on both the desired and the actual side:

```pkl theme={"languages":{"custom":["/languages/pkl.json"]}}
@formae.FieldHint { updateMethod = "Atomic"; preserveEmptyValues = true }
spec: Any?
```

Typically paired with `updateMethod = "Atomic"`: a field opaque enough that its empties are meaningful is usually also one formae cannot safely diff member by member. The hint is honored on top-level resource fields only; on sub-resource properties it has no effect.

Two things it does not change. Absence stays absence: when the user omits the field entirely and the provider's read echoes an empty collection back, formae still does not plan a removal. And other transformations keep applying inside the field: cross-resource references still resolve, and output shaping still runs.

<Note>
  Requires formae 0.89.0 or later, the `formae.pkl` schema package at 0.89.0 or later, and `pkg/model` v0.1.27 or later. An older core would apply atomic diffing without the fidelity, silently reproducing the empty-value cleanup the hint exists to prevent, so raise your plugin's `minFormaeVersion` alongside adopting the hint.
</Note>

#### format

Declares that a String field holds serialized structured content, so formae compares its canonical content rather than raw bytes:

```pkl theme={"languages":{"custom":["/languages/pkl.json"]}}
@formae.FieldHint { format = "json" }
dashboard: String
```

| Value         | Behavior                                                                                 |
| ------------- | ---------------------------------------------------------------------------------------- |
| (none / `""`) | Opaque String, compared byte for byte (default)                                          |
| `"json"`      | Parsed and canonicalized as JSON before comparison; key order and whitespace are ignored |

`"json"` is the only recognized format today. Use it when a provider re-serializes a document into an equivalent but differently formatted form, which would otherwise surface as drift on every sync.

#### Output shaping

When the key or value you render differs from what the user declares, use `outputField`, `outputTransformation`, and `outputCondition`. `outputField` overrides the rendered key; `outputTransformation` maps the value; `outputCondition` gates whether the transformed value is emitted at all:

```pkl theme={"languages":{"custom":["/languages/pkl.json"]}}
@formae.FieldHint {
    outputField = "Tags"
    outputTransformation = (it) -> formae.transform.capitalizeMemberKeys.apply(it)
}
tags: Listing<Tag>?
```

The `formae.transform` helper carries ready-made transforms: `capitalizeMemberKeys`, `capitalizeKeys`, and `toString`.

### @formae.ConfigFieldHint

Applied to a target Config field to declare whether it can change in place or requires a target replace. Fields without this annotation are treated as immutable.

| Field        | Type      | Default | Description                                                                                  |
| ------------ | --------- | ------- | -------------------------------------------------------------------------------------------- |
| `createOnly` | `Boolean` | `true`  | When `true`, changing the field triggers a target replace; when `false`, it updates in place |

```pkl theme={"languages":{"custom":["/languages/pkl.json"]}}
open class Config {
    hidden fixed type: String = "MYCLOUD"

    /// Credential profile. Can change without recreating resources.
    @formae.ConfigFieldHint { createOnly = false }
    hidden profile: String?

    /// Deployment region. Changing it requires a full target replace.
    @formae.ConfigFieldHint { createOnly = true }
    hidden region: String

    fixed Type: String = type
    fixed Profile: String? = profile
    fixed Region: String = region
}
```

Note that `ConfigFieldHint.createOnly` defaults to `true` (the safe, immutable default), which is the opposite of `FieldHint.createOnly`. When a target Config carries `ConfigFieldHint` annotations, formae auto-generates a [`ConfigSchema`](#configschema) on the target output describing each field's mutability. See [Replacing a target](/documentation/concepts/target#replacing-a-target) for the user-facing behavior.

***

## Relationships and ordering

### EdgeKind (typealias)

`EdgeKind` classifies the dependency edge a cross-resource reference creates, which drives create and destroy ordering. Its three allowed values:

| Value                 | Meaning                                                                                                                                    | Ordering                                                                                                               |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
| `"default"`           | Construction data flow. The default for unannotated reference fields.                                                                      | On destroy: consumer first, producer second.                                                                           |
| `"attachesTo"`        | Reachability. The consumer is reachable through the producer at runtime (for example an ECS Service through an ALB Listener).              | On destroy: producer first, consumer second.                                                                           |
| `"runtimeDependency"` | Containment. The consumer uses the producer and its containment children at runtime, even when those children are not directly referenced. | On create: producer first, then each child, then consumer. On destroy: consumer first, then each child, then producer. |

Set it through `@formae.FieldHint { edgeKind = "..." }` on the referencing field.

### ParentRef

Maps a parent-side identity property to the child-side property that carries its value. The two names need not match. Used in `ResourceHint.parentRefs`.

| Field            | Type     | Description                                             |
| ---------------- | -------- | ------------------------------------------------------- |
| `parentProperty` | `String` | Property on the parent resource this mapping references |
| `childProperty`  | `String` | Property on this child that holds the parent's value    |

```pkl theme={"languages":{"custom":["/languages/pkl.json"]}}
new formae.ParentRef {
    parentProperty = "ServiceName"
    childProperty = "Service"
}
```

### ListProperty

The legacy relationship type used in `ResourceHint.listParam`. It names the parent property and the List API parameter that carries the parent's value on the child.

| Field            | Type     | Description                                                          |
| ---------------- | -------- | -------------------------------------------------------------------- |
| `parentProperty` | `String` | Name of the property on the parent resource                          |
| `listParameter`  | `String` | Name of the parameter the List API accepts to query nested resources |

```pkl theme={"languages":{"custom":["/languages/pkl.json"]}}
new formae.ListProperty {
    parentProperty = "BucketName"
    listParameter = "Bucket"
}
```

When formae derives a parent mapping from `listParam`, the child-side property name is taken from `listParameter`.

### FieldUpdateMethod (typealias)

The set of collection comparison strategies accepted by `FieldHint.updateMethod`:

```pkl theme={"languages":{"custom":["/languages/pkl.json"]}}
typealias FieldUpdateMethod = "Array" | "Atomic" | "EntitySet" | "Set"
```

See [updateMethod](#updatemethod-and-indexfield) above and [Collection semantics](/plugin-development/concepts/collection-semantics).

***

## Value types you emit

These are the types a resource's properties can carry beyond plain scalars, lists, and maps.

### Value

`Value` wraps a String property with visibility and update-strategy metadata. It has two dimensions:

| Field        | Type                    | Default    | Description                                               |
| ------------ | ----------------------- | ---------- | --------------------------------------------------------- |
| `value`      | `String`                | required   | The underlying string value                               |
| `visibility` | `"Clear" \| "Opaque"`   | `"Clear"`  | `Opaque` marks the value as secret                        |
| `strategy`   | `"Update" \| "SetOnce"` | `"Update"` | `SetOnce` writes the value on create and never updates it |

Construct one with the `formae.value(...)` function, then optionally chain the `.opaque` or `.setOnce` helpers:

```pkl theme={"languages":{"custom":["/languages/pkl.json"]}}
// A clear, updatable value
someField = formae.value("hello")

// A secret that is set once and never updated
apiKey = formae.value("s3cr3t").opaque.setOnce
```

<Note>
  You do not handle `Value` specially in your plugin code. formae core strips the value, visibility, and strategy metadata before your plugin sees properties, and adds it back on return. Your plugin receives and returns plain JSON. You would only see `Value` metadata when debugging raw properties.
</Note>

See the user-facing [Values](/documentation/concepts/values) concept for how end users work with these.

### SecretValue

`SecretValue` is an opaque variant of `Value` for secret fields. Type a property `(String | formae.SecretValue)` (nullable and union variants included), and formae derives the field's `opaque` hint from the type: the agent stores the value as a one-way hash wherever it persists state (apply, sync, discovery, drift, and extract) and keeps it out of logs. The user still supplies a plain string; the field is opaque whatever value they set.

```pkl theme={"languages":{"custom":["/languages/pkl.json"]}}
@formae.FieldHint {}
masterPassword: (String|formae.SecretValue)?
```

This is the schema-side equivalent of `formae.value(...).opaque`. Chain `.setOnce` for a secret that is written on create and never updated. Being opaque is independent of `writeOnly`, which controls read-back and drift; a secret the provider never returns is typically both.

### Prop

`Prop` reads a value from a CLI flag (`prop:<flag>`), falling back to a default. It infers its type from the default's type (`String`, `Boolean`, `Int`, or `Float`).

| Field     | Type                             | Default | Description                        |
| --------- | -------------------------------- | ------- | ---------------------------------- |
| `flag`    | `String?`                        | `null`  | The `prop:` flag name to read      |
| `default` | `(String\|Boolean\|Int\|Float)?` | `null`  | Value used when the flag is absent |

```pkl theme={"languages":{"custom":["/languages/pkl.json"]}}
hidden replicas = new formae.Prop {
    flag = "replicas"
    default = 3
}
```

The related module-level helpers are `formae.flag(key)` (reads a `prop:` value or returns `null`), `formae.cmd()`, and `formae.mode()`.

### Tag

<Warning>
  `formae.Tag` is deprecated and will be removed in a future version. AWS resources should use `aws.Tag` from `@aws/aws.pkl`. It is documented here only because it still exists in the schema.
</Warning>

A simple key/value pair with fields `key: String` and `value: Any`, rendered as `Key` and `Value`.

### Resolvables and embedded values

Reference fields that point at another resource's output use the `Resolvable` family (`Resolvable`, `MappingResolvable`, `ListingResolvable`, and the `StackResolvable` / `TargetResolvable` / `PolicyResolvable` variants). A `Resolvable` carries `label`, `type`, `stack`, `property`, and a `visibility` of `"Clear"` or `"Opaque"` (with an `.opaque` helper). `MappingResolvable.at(key)` and `ListingResolvable.at(index)` produce a resolvable that points at a nested element of the resolved value.

A String field that embeds a resolvable inside literal text uses `formae.embed(...)`, which produces an `Embedded` value the agent substitutes at resolve time before shipping a plain String to your plugin.

For how to declare a resolvable class, expose properties through `hidden res`, and accept references on your fields, see the plugin [Resolvables](/plugin-development/concepts/resolvables) and [Embedded types](/plugin-development/concepts/embedded-types) pages. The user-facing [Resolvable](/documentation/concepts/resolvable) concept covers how end users reference them in a forma.

***

## Schema module shape

You rarely reference these output classes directly; formae derives them from your annotations at evaluation time. They are documented so you can read evaluated output and understand what your annotations produce.

### Schema

`Schema` is the resource descriptor formae builds for each resource class from its `ResourceHint` and the `FieldHint`s on its properties.

| Field            | Type                          | Default          | Source                                                   |
| ---------------- | ----------------------------- | ---------------- | -------------------------------------------------------- |
| `Identifier`     | `String`                      | required         | `ResourceHint.identifier`                                |
| `Fields`         | `Listing<String>`             | required         | All non-hidden annotated property names                  |
| `Hints`          | `Mapping<String, FieldHint>?` | `null`           | Per-field hints, including dotted keys for sub-resources |
| `Discoverable`   | `Boolean`                     | `true`           | `ResourceHint.discoverable`                              |
| `Extractable`    | `Boolean`                     | `true`           | `ResourceHint.extractable`                               |
| `Portable`       | `Boolean`                     | `false`          | `ResourceHint.portable`                                  |
| `Parent`         | `String`                      | `""`             | `ResourceHint.parent` (empty when none)                  |
| `ParentMappings` | `Listing<ParentRef>`          | `new Listing {}` | Derived from `parentRefs`, falling back to `listParam`   |

`Schema` also exposes `query(prop)`, which returns the field names whose `FieldHint` has a given boolean property set (for example the createOnly fields).

### ConfigSchema

`ConfigSchema` describes the per-field mutability of a target's Config. It holds a single `Hints: Mapping<String, ConfigFieldHint>?`. formae generates it automatically from the `@formae.ConfigFieldHint` annotations on a target's Config class; you do not construct it by hand.

***

## See also

* [02 - Resource schema](/plugin-development/tutorial/02-schema) - hands-on schema walkthrough
* [Parent and child resources](/plugin-development/concepts/parent-child-resources) - `parent`, `parentRefs`, `listParam`
* [Collection semantics](/plugin-development/concepts/collection-semantics) - `updateMethod` and `indexField`
* [Resolvables](/plugin-development/concepts/resolvables) and [Embedded types](/plugin-development/concepts/embedded-types)
* [Plugin manifest reference](/plugin-development/reference/manifest) - `namespace` and plugin metadata
* [Plugin interface reference](/plugin-development/reference/plugin-interface) - the CRUD operations your schema drives
* [PKL cheatsheet](/documentation/reference/pkl-cheatsheet) - PKL language basics
* [Values](/documentation/concepts/values) and [Resolvable](/documentation/concepts/resolvable) - user-facing concepts
