Skip to main content
This section defines the 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 of schema/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
The 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:
  • content has @formae.FieldHint {}, so it can be updated
  • path and permissions have createOnly = true, so changing either requires replacing the file

Type field

Every resource needs a type field. Use fixed hidden to make it a constant that doesn’t appear in user-facing output:
  • fixed: value cannot be changed
  • hidden: not shown in output (formae adds it automatically)

Validate the schema

Use the PKL CLI to check for syntax errors:
If valid, you’ll see the evaluated output. If there are errors, PKL will show the line number and issue.

Verify schema conventions

Beyond PKL syntax, formae provides a verify-schema tool that checks your schema follows formae conventions:
This validates:
  • 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.ResourceHint with type and identifier
Run this whenever you add or modify schema files. It catches mistakes early that would otherwise cause confusing errors at runtime, like a typo in your namespace or accidentally defining the same type in two files. For plugins with many resource types (like AWS with 200+ types), this check becomes essential for maintaining schema integrity.

Rebuild the plugin

After changing the schema, rebuild:
The SDK reads schemas at startup, so the binary itself doesn’t change, but rebuilding verifies everything still compiles.

Understanding the identifier

The identifier = "$.path" uses JSONPath to tell formae how to find the native ID in your resource’s JSON. When your Create method returns:
formae extracts /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.