Skip to content

01 - Project Scaffold

This section walks through creating a new plugin project using formae plugin init.

Initialize the Plugin

Run formae plugin init to start the interactive setup:

formae plugin init

The command walks you through several prompts:

  1. Plugin name - A short identifier for your plugin (lowercase, letters/numbers/hyphens). We'll use sftp.

  2. Namespace - The prefix for your resource types (uppercase). We'll use SFTP, which means our resources will have types like SFTP::Files::File.

  3. Description - A brief summary of what the plugin does. For this tutorial: SFTP file management plugin for <span class="product-name">formae</span>.

  4. Author - Your name or organization. Enter whatever identifies you as the plugin developer.

  5. License - Choose from common open-source licenses (MIT, Apache-2.0, etc.) or select "Other" to provide a custom SPDX identifier. Unless you select "Other", the command generates the appropriate LICENSE file automatically.

  6. Target directory - Where to create the plugin project. Defaults to ./<plugin-name>, so we'll get ./formae-plugin-sftp.

After answering the prompts, the command downloads a template and creates a complete project structure.

Project Structure

formae-plugin-sftp/
├── formae-plugin.pkl      # Plugin manifest
├── main.go                # Entry point
├── sftp.go                # ResourcePlugin implementation (stub)
├── schema/
│   └── pkl/
│       ├── PklProject     # PKL dependencies
│       └── sftp.pkl       # Resource schemas (example)
├── conformance_test.go    # Conformance test setup
├── scripts/
│   └── ci/
│       └── clean-environment.sh
├── examples/              # Usage examples
├── .github/
│   └── workflows/         # CI configuration
├── go.mod
├── go.sum
├── Makefile
└── README.md

Makefile

The template includes a Makefile with common development tasks:

Command Description
make build Build the plugin binary to bin/
make test Run all tests
make test-unit Run unit tests only (tests tagged with //go:build unit)
make test-integration Run integration tests (tests tagged with //go:build integration)
make lint Run golangci-lint
make install Build and install the plugin locally to ~/.pel/formae/plugins/
make conformance-test Run conformance tests against a running formae agent
make clean Remove build artifacts

The install target copies the binary, schema files, and manifest to the local plugin directory where the formae agent can discover it.

Key Files

formae-plugin.pkl

The manifest contains the information the formae agent needs to know about your plugin:

name = "sftp"
version = "0.1.0"
namespace = "SFTP"
description = "SFTP file management plugin for formae"
license = "FSL-1.1-ALv2"
minFormaeVersion = "0.80.0"

output {
  renderer = new JsonRenderer {}
}
Field Description
name Plugin identifier (lowercase, letters/numbers/hyphens)
version Semantic version
namespace Resource type prefix (uppercase). Appears in types like SFTP::Files::File
license SPDX license identifier
minFormaeVersion Minimum formae version required

main.go

The entry point starts the SDK and should never be modified:

package main

import "github.com/platform-engineering-labs/formae/pkg/plugin/sdk"

func main() {
    sdk.RunWithManifest(&Plugin{}, sdk.RunConfig{})
}

RunWithManifest takes care of all plugin initialization - reading the manifest, loading schemas, and handling communication with the formae agent. Your job is to implement the Plugin struct; the SDK handles everything else.

sftp.go

The template provides a stub implementation with all required methods:

type Plugin struct{}

var _ plugin.ResourcePlugin = &Plugin{}

// Configuration methods
func (p *Plugin) RateLimit() plugin.RateLimitConfig { ... }
func (p *Plugin) DiscoveryFilters() []plugin.MatchFilter { ... }
func (p *Plugin) LabelConfig() plugin.LabelConfig { ... }

// CRUD operations (stubs that return ErrNotImplemented)
func (p *Plugin) Create(ctx context.Context, req *resource.CreateRequest) (*resource.CreateResult, error) { ... }
func (p *Plugin) Read(ctx context.Context, req *resource.ReadRequest) (*resource.ReadResult, error) { ... }
func (p *Plugin) Update(ctx context.Context, req *resource.UpdateRequest) (*resource.UpdateResult, error) { ... }
func (p *Plugin) Delete(ctx context.Context, req *resource.DeleteRequest) (*resource.DeleteResult, error) { ... }
func (p *Plugin) Status(ctx context.Context, req *resource.StatusRequest) (*resource.StatusResult, error) { ... }
func (p *Plugin) List(ctx context.Context, req *resource.ListRequest) (*resource.ListResult, error) { ... }

go.mod

The module imports the formae plugin SDK and conformance test packages:

module github.com/platform-engineering-labs/formae-plugin-sftp

go 1.25

require (
    github.com/platform-engineering-labs/formae/pkg/plugin v0.1.0
    github.com/platform-engineering-labs/formae/pkg/plugin-conformance-tests v0.1.0
)

Verify the Setup

Build the plugin to verify everything compiles:

cd formae-plugin-sftp
make build

You should see output like:

Building sftp plugin...
go build -o bin/sftp .

The binary is created at bin/sftp.

What's Next

The template includes an example resource schema to get you started. In the next section, we'll replace it with our own File resource schema that defines the properties for managing files on an SFTP server.


Next: 02 - Resource Schema - Define the File resource in PKL