@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:
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 patternNAMESPACE::SERVICE::RESOURCE (three segments separated by ::):
NAMESPACE: your plugin’s namespace (uppercase), matching thenamespacein your plugin manifest.SERVICE: a logical service grouping, for exampleEC2orStorage.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: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.
List(...) of either type.
discoverable, extractable, portable
Each is a boolean gate:discoverable = falsekeeps internal or high-volume resources out of discovery results.extractable = falsekeeps a resource out of CLI extraction output (useful for auto-generated or internal state).portable = truemarks 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 aformae.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.
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 setopaque 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: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, useoutputField, outputTransformation, and outputCondition. outputField overrides the rendered key; outputTransformation maps the value; outputCondition gates whether the transformed value is emitted at all:
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.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 inResourceHint.parentRefs.
ListProperty
The legacy relationship type used inResourceHint.listParam. It names the parent property and the List API parameter that carries the parent’s value on the child.
listParam, the child-side property name is taken from listParameter.
FieldUpdateMethod (typealias)
The set of collection comparison strategies accepted byFieldHint.updateMethod:
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.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.
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).
formae.flag(key) (reads a prop: value or returns null), formae.cmd(), and formae.mode().
Tag
A simple key/value pair with fieldskey: String and value: Any, rendered as Key and Value.
Resolvables and embedded values
Reference fields that point at another resource’s output use theResolvable 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
- 02 - Resource schema - hands-on schema walkthrough
- Parent and child resources -
parent,parentRefs,listParam - Collection semantics -
updateMethodandindexField - Resolvables and Embedded types
- Plugin manifest reference -
namespaceand plugin metadata - Plugin interface reference - the CRUD operations your schema drives
- PKL cheatsheet - PKL language basics
- Values and Resolvable - user-facing concepts

