File resource type in PKL. The schema describes the properties of files we’ll manage on SFTP servers.
Most plugins define multiple resource types. An AWS plugin, for example, might have EC2::Instance, S3::Bucket, RDS::Database, and dozens more. Our SFTP plugin only needs one resource type (File), which keeps the tutorial focused. If you’re building a plugin with multiple resource types, see 15 - Real-World Plugins for patterns on organizing schemas and dispatching operations by resource type.
Design the resource
Before writing the schema, think about the properties of a file on an SFTP server:
The
path is the identifier: it uniquely identifies the file on the server.
Note that we don’t include computed values like file size or modification time in the schema. The schema defines what users declare. Computed values are returned by your plugin’s Read operation but aren’t part of the declarative specification.
Write the schema
Replace the contents ofschema/pkl/sftp.pkl:
Schema annotations
PKL annotations provide metadata that tells formae how to handle your resources and fields. For complete documentation of all annotation options, see the schema reference.@formae.ResourceHint
Every resource class needs a@formae.ResourceHint annotation with two required fields:
- type: the full resource type following the pattern
NAMESPACE::SERVICE::RESOURCE - identifier: a JSONPath expression that extracts the unique ID from the resource’s JSON representation
identifier is critical: when your plugin returns resource properties, formae uses this path to extract the native ID. Here we use $.path because the file path uniquely identifies the file on the server.
@formae.FieldHint
Every property needs a@formae.FieldHint annotation for formae to track it. Without this annotation, formae won’t detect changes to the field during updates.
In our schema:
contenthas@formae.FieldHint {}, so it can be updatedpathandpermissionshavecreateOnly = true, so changing either requires replacing the file
Type field
Every resource needs atype field. Use fixed hidden to make it a constant that doesn’t appear in user-facing output:
fixed: value cannot be changedhidden: not shown in output (formae adds it automatically)
Validate the schema
Use the PKL CLI to check for syntax errors:Verify schema conventions
Beyond PKL syntax, formae provides averify-schema tool that checks your schema follows formae conventions:
- Resource type naming: types must start with your plugin’s namespace (e.g.,
SFTP::) - No duplicate types: each resource type must be defined exactly once
- No duplicate files: schema files must have unique names across subdirectories
- Required annotations: resources have
@formae.ResourceHintwithtypeandidentifier
Rebuild the plugin
After changing the schema, rebuild:Understanding the identifier
Theidentifier = "$.path" uses JSONPath to tell formae how to find the native ID in your resource’s JSON. When your Create method returns:
/data/config.txt as the native ID using the JSONPath $.path.
This native ID is then passed to Read, Update, and Delete requests so your plugin knows which file to operate on.
Next: 03 - Target Configuration
See also: advanced patterns
For patterns beyond this introduction, see canonical examples in production plugins:- Polymorphic resources / discriminated subtypes (e.g., resources with abstract base types and concrete variants like Kubernetes auth strategies):
formae-plugin-k8s/schema/pkl/main/k8s.pkl - Computed/Resolvable outputs on a resource (custom Read, output-port story):
formae-plugin-aws/schema/pkl/ses/emailidentity.pkl - Synthetic identifiers (
identifier = "Label"for logical resources without natural server-side IDs): see the atlas plugin’s Migration resource.

