Skip to main content
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. For PKL language basics, see the 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:
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. 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 ::):
  • NAMESPACE: your plugin’s namespace (uppercase), matching the namespace in your plugin 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:
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 for the full model. parent names the parent resource type:
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:
listParam is the legacy way to express the same relationship for List operations, using 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.
For resources that need multiple parent properties, pass a List(...) of either type.

discoverable, extractable, portable

Each is a boolean gate:
  • 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.
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. There are also three output-shaping fields, all hidden, used when the value you render differs from what the user declares:
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.

createOnly

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

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:
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.

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:

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:

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:
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 type alias. Set it on the field that references another resource:
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 type alias. indexField names the key property for EntitySet collections. See Collection semantics for the full model.

format

Declares that a String field holds serialized structured content, so formae compares its canonical content rather than raw bytes:
"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:
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.
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 on the target output describing each field’s mutability. See 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: 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.

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.
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:
See updateMethod above and 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: Construct one with the formae.value(...) function, then optionally chain the .opaque or .setOnce helpers:
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.
See the user-facing 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.
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).
The related module-level helpers are formae.flag(key) (reads a prop: value or returns null), formae.cmd(), and formae.mode().

Tag

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.
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 and Embedded types pages. The user-facing 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 FieldHints on its properties. 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