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:
-
Plugin name - A short identifier for your plugin (lowercase, letters/numbers/hyphens). We'll use
sftp. -
Namespace - The prefix for your resource types (uppercase). We'll use
SFTP, which means our resources will have types likeSFTP::Files::File. -
Description - A brief summary of what the plugin does. For this tutorial:
SFTP file management plugin for <span class="product-name">formae</span>. -
Author - Your name or organization. Enter whatever identifies you as the plugin developer.
-
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.
-
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