Skip to content

02 - Resource Schema

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 - for example, an AWS plugin 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:

Property Type Mutability Description
path String Immutable File path on the server. Changing it means a different file.
content String Mutable Text content of the file
permissions String Immutable Unix permissions (e.g., "0644"). Changing requires replace.

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:

module sftp

import "@formae/formae.pkl"

/// A text file on an SFTP server.
@formae.ResourceHint {
    type = "SFTP::Files::File"
    identifier = "$.path"
}
class File extends formae.Resource {
    fixed hidden type: String = "SFTP::Files::File"

    /// Path to the file on the SFTP server.
    /// Cannot be changed after creation - changing the path means a different file.
    @formae.FieldHint { createOnly = true }
    path: String

    /// Text content of the file.
    @formae.FieldHint {}
    content: String

    /// Unix file permissions (e.g., "0644", "0755").
    /// Defaults to "0644" if not specified.
    @formae.FieldHint { createOnly = true }
    permissions: String = "0644"
}

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:

@formae.ResourceHint {
    type = "SFTP::Files::File"
    identifier = "$.path"
}
  • 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.

// Mutable property - can be changed after creation
@formae.FieldHint {}
content: String

// Immutable property - changing it forces resource replacement
@formae.FieldHint { createOnly = true }
path: String
Option Meaning
{} (empty) Normal mutable property - can be updated in place
createOnly = true Immutable - changing it forces a replace (delete + create)

In our schema: - content has @formae.FieldHint {} - it can be updated - path and permissions have createOnly = true - 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 hidden type: String = "SFTP::Files::File"
  • 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:

cd formae-plugin-sftp
formae eval schema/pkl/sftp.pkl

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:

make verify-schema

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:

make build

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:

{
  "path": "/data/config.txt",
  "content": "debug=true",
  "permissions": "0644"
}

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