Schema Annotations
This page documents all PKL annotations used in formae resource schemas.
Overview
Annotations provide metadata about resources and their fields. The SDK reads these annotations to:
- Generate resource descriptors
- Validate user input
- Control CRUD behavior
- Enable discovery and extraction
ResourceHint
Applied to resource classes to define type metadata.
@formae.ResourceHint {
type = "MYCLOUD::Compute::Instance"
identifier = "$.InstanceId"
}
class Instance extends formae.Resource {
// ...
}
Fields
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
type |
String |
Yes | — | Full resource type name |
identifier |
String |
Yes | — | JSONPath to native ID in API response |
parent |
String |
No | null |
Parent resource type for nested resources |
listParam |
ListProperty |
No | null |
Parent property required for List operations |
discoverable |
Boolean |
No | true |
If false, excluded from discovery |
extractable |
Boolean |
No | true |
If false, excluded from formae extract output |
portable |
Boolean |
No | false |
If true, the resource can be safely recreated on a different target |
type
Full resource type name following the pattern NAMESPACE::SERVICE::RESOURCE:
type = "AWS::EC2::Instance"
type = "AZURE::Compute::VirtualMachine"
type = "MYCLOUD::Storage::Bucket"
Naming conventions:
NAMESPACE- Your plugin's namespace (uppercase)SERVICE- Logical service grouping (e.g.,EC2,S3,IAM)RESOURCE- Resource name (PascalCase)
identifier
JSONPath expression to extract the native ID from your cloud API's response:
identifier = "$.InstanceId" // Top-level field
identifier = "$.Resource.Id" // Nested field
identifier = "$.Arn" // ARN as identifier
identifier = "$.Metadata.Guid" // Deep nested
The native ID must:
- Be unique within the resource type
- Be stable (not change over the resource's lifetime)
- Be returned by your Create operation
parent and listParam
For child resources, use parent to declare the parent resource type and listParam to specify which parent properties are needed for the List operation:
@formae.ResourceHint {
type = "MYCLOUD::Storage::Object"
identifier = "$.ObjectKey"
parent = "MYCLOUD::Storage::Bucket"
listParam = new formae.ListProperty {
parentProperty = "BucketName" // Property on the parent resource
listParameter = "Bucket" // Parameter name the List API expects
}
}
class Object extends formae.Resource {
// ...
}
This enables:
- Scoped discovery (list objects within a specific bucket)
- Correct ordering (discover parents before children)
- Automatic resolvable injection from child to parent
For resources requiring multiple parent properties:
listParam = List(
new formae.ListProperty {
parentProperty = "TableName"
listParameter = "TableName"
},
new formae.ListProperty {
parentProperty = "IndexName"
listParameter = "IndexName"
}
)
During discovery, formae reads the parent's properties and passes them to your List operation via ListRequest.ListParameters.
discoverable
Set to false to exclude from automatic discovery:
@formae.ResourceHint {
type = "MYCLOUD::Internal::TempResource"
identifier = "$.Id"
discoverable = false // Don't show in discovery
}
Use for:
- Internal/system resources users shouldn't manage
- Resources that would flood discovery results
- Temporary or ephemeral resources
extractable
Set to false to exclude from formae extract output:
@formae.ResourceHint {
type = "MYCLOUD::Internal::AuditLog"
identifier = "$.LogId"
extractable = false // Don't include in extract output
}
Use for resources that are managed by formae but shouldn't be exported back to PKL (e.g., auto-generated resources, internal state).
portable
Set to true to indicate the resource can be safely recreated on a different target (e.g., when a target's region changes):
@formae.ResourceHint {
type = "MYCLOUD::IAM::Role"
identifier = "$.RoleName"
portable = true // Can be recreated on a different target
}
When a target replace is triggered, formae checks that all resources on the target are portable before proceeding. Non-portable resources (the default) block target replacement. This prevents two classes of problems:
- Data loss: Region-bound resources like databases or storage volumes cannot be moved — they must be destroyed and recreated, losing their data.
- Invalid configuration: Some resource properties are region-specific and won't work on another target. For example, an EC2 instance's AMI ID is only valid in the region where it was published.
FieldHint
Applied to resource properties to define field metadata.
@formae.FieldHint {
createOnly = true
description = "The image ID to launch from"
}
imageId: String
Fields
| Field | Type | Default | Description |
|---|---|---|---|
createOnly |
Boolean |
false |
Cannot be changed after creation |
writeOnly |
Boolean |
false |
Can be written but never returned by Read |
required |
Boolean |
false |
Must be provided on every create and update |
requiredOnCreate |
Boolean |
false |
Must be provided on create, optional on update |
hasProviderDefault |
Boolean |
false |
Provider assigns a default if not specified |
updateMethod |
String |
null |
Collection comparison strategy: "Array", "EntitySet", "Set", or "Atomic". See Collection Semantics |
indexField |
String |
null |
Key field for EntitySet collections. See Collection Semantics |
createOnly
Fields that cannot be modified after the resource is created:
@formae.FieldHint { createOnly = true }
region: String
@formae.FieldHint { createOnly = true }
encryptionKeyId: String
Attempting to change a createOnly field triggers a replace operation (delete + create).
writeOnly
Fields that can be written but are never returned by the cloud provider's Read operation:
@formae.FieldHint { writeOnly = true }
password: String
@formae.FieldHint { writeOnly = true }
secretKey: String
WriteOnly fields:
- Are stored by formae but never returned by the provider
- Are always included in update patches (even if unchanged)
- Common for passwords, secrets, and sensitive configuration
This is essential for cloud APIs (like AWS CloudControl) that require certain fields in every update but never return them on read.
hasProviderDefault
Fields where the cloud provider assigns a default value if the user doesn't specify one:
@formae.FieldHint { hasProviderDefault = true }
bucketEncryption: BucketEncryption?
@formae.FieldHint { hasProviderDefault = true }
visibilityTimeout: Int?
HasProviderDefault fields:
- Accept provider-assigned defaults when not specified by the user
- Prevent "remove" operations that would delete the provider's default
- Still allow user overrides when explicitly specified
- Eliminate oscillation during reconcile cycles
Example scenario: AWS S3 buckets have encryption enabled by default (AES256). Without this hint, if a user doesn't specify BucketEncryption in their PKL, formae would generate a "remove" operation to delete the encryption—causing oscillation on every reconcile as AWS re-applies the default.
With hasProviderDefault = true:
- User omits field → accept provider's default (no patch generated)
- User specifies field → apply user's value (patch generated if different)
Comparison with writeOnly:
| Hint | Behavior |
|---|---|
writeOnly |
Always removes field from actual state before comparison |
hasProviderDefault |
Only removes field from actual state if NOT in desired state |
required
Fields that must be provided on every create and update:
@formae.FieldHint { required = true }
name: String
Required fields are validated before sending to the plugin. If a required field is missing, the command fails with a validation error.
requiredOnCreate
Fields that must be provided on create but are optional on update:
@formae.FieldHint { requiredOnCreate = true }
initialPassword: String?
updateMethod
Controls how collections (arrays, maps) are compared between desired and actual state. See Collection Semantics for full details.
@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 |
"EntitySet" |
Key-based comparison (requires indexField) |
"Set" |
Explicit unordered set |
"Atomic" |
Opaque value — single replace, no sub-field diffing |
indexField
The key field for EntitySet collections. Used with updateMethod = "EntitySet" to identify elements by a unique key:
@formae.FieldHint { updateMethod = "EntitySet"; indexField = "Key" }
tags: Listing<Tag>?
ConfigFieldHint
Applied to target config fields to declare whether a field can be changed in place (mutable) or requires a full target replace (immutable).
@formae.ConfigFieldHint { createOnly = false }
hidden profile: String?
@formae.ConfigFieldHint { createOnly = true }
hidden region: Region
Fields
| Field | Type | Default | Description |
|---|---|---|---|
createOnly |
Boolean |
true |
If true, changing this field triggers a target replace. If false, the field can be updated in place. |
createOnly
Controls whether changing a config field requires recreating the target and all its resources:
createOnly = true(default) — the field is immutable. Changing it triggers a full target replace: delete resources, delete target, create target, recreate resources.createOnly = false— the field is mutable. Changing it produces an in-place target update without affecting resources.
Fields without the @formae.ConfigFieldHint annotation are treated as immutable (equivalent to createOnly = true). This is the safe default.
ConfigSchema
When a target's Config class contains @formae.ConfigFieldHint annotations, formae auto-generates a ConfigSchema on the target output. The ConfigSchema contains a Hints mapping that describes the mutability of each annotated field. You do not need to create this manually — it is derived from the annotations at evaluation time.
Example
A complete target config class with per-field mutability:
import "@formae/formae.pkl"
open class Config {
hidden fixed type: String = "MYCLOUD"
/// The credential profile to use. Can be changed without recreating resources.
@formae.ConfigFieldHint { createOnly = false }
hidden profile: String?
/// The deployment region. Changing this requires a full target replace.
@formae.ConfigFieldHint { createOnly = true }
hidden region: String
fixed Type: String = type
fixed Profile: String? = profile
fixed Region: String = region
}
When a user changes profile, formae updates the target in place. When a user changes region, formae triggers a full target replace. Use formae apply --simulate to preview which fields changed and whether the operation will be an update or a replace.
See Per-field config mutability for the user-facing documentation.
Type Examples
Required vs Optional
// Required - must be provided
@formae.FieldHint {}
name: String
// Optional - can be omitted
@formae.FieldHint {}
description: String?
With Defaults
@formae.FieldHint {}
instanceType: String = "small"
@formae.FieldHint {}
enabled: Boolean = true
@formae.FieldHint {}
maxRetries: Int = 3
Collections
// Map of strings
@formae.FieldHint {}
tags: Mapping<String, String>
// List of strings
@formae.FieldHint {}
securityGroups: Listing<String>
// Optional list
@formae.FieldHint {}
additionalIps: Listing<String>?
Nested Types
@formae.FieldHint {}
networkConfig: NetworkConfig?
class NetworkConfig extends formae.SubResource {
@formae.FieldHint {}
vpcId: String
@formae.FieldHint {}
subnetIds: Listing<String>
@formae.FieldHint {}
assignPublicIp: Boolean = false
}
Complete Example
module mycloud
import "@formae/formae.pkl"
/// A compute instance in MyCloud.
@formae.ResourceHint {
type = "MYCLOUD::Compute::Instance"
identifier = "$.InstanceId"
portable = true
}
class Instance extends formae.Resource {
fixed hidden type: String = "MYCLOUD::Compute::Instance"
/// Display name for the instance.
@formae.FieldHint { required = true }
name: String
/// Machine type determining CPU and memory.
@formae.FieldHint {}
instanceType: String = "small"
/// Image to launch from. Cannot be changed after creation.
@formae.FieldHint { createOnly = true }
imageId: String
/// Region where the instance runs.
@formae.FieldHint { createOnly = true }
region: String
/// Security group IDs. Provider assigns a default if not specified.
@formae.FieldHint { hasProviderDefault = true }
securityGroups: Listing<String>?
/// Tags for organization.
@formae.FieldHint { updateMethod = "EntitySet"; indexField = "Key" }
tags: Listing<Tag>?
}
See Also
- Pkl cheatsheet - PKL language basics
- Tutorial: Schema - Step-by-step schema creation