> ## Documentation Index
> Fetch the complete documentation index at: https://docs.formae.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Build an SFTP plugin

> Tutorial: build a complete formae plugin that manages files on an SFTP server, from scaffold to conformance tests.

This tutorial walks you through building a complete formae plugin that manages files on an SFTP server. By the end, you'll have a production-ready plugin with:

* A `File` resource type with mutable and immutable properties
* Full CRUD operations (Create, Read, Update, Delete)
* Discovery to find existing files on the server
* Integration tests and conformance tests
* CI configuration for GitHub Actions

## Prerequisites

Before starting, ensure you have:

* **Go 1.25 or later**: `go version`
* **PKL CLI**: [Installation guide](https://pkl-lang.org/main/current/pkl-cli/index.html)
* **formae CLI**: installed and accessible in your PATH
* **Docker**: for running the test SFTP server

## Test server setup

We'll use a Docker-based SFTP server for development and testing:

```bash theme={"languages":{"custom":["/languages/pkl.json"]}}
docker run -p 2222:22 -d --name sftp-test atmoz/sftp testuser:testpass:::upload
```

This creates an SFTP server with:

* **Host:** localhost
* **Port:** 2222
* **Username:** testuser
* **Password:** testpass
* **Writable directory:** /upload

Verify it's running:

```bash theme={"languages":{"custom":["/languages/pkl.json"]}}
sftp -P 2222 testuser@localhost
# Enter password: testpass
# sftp> ls
# sftp> exit
```

## Tutorial structure

This tutorial uses test-driven development (TDD). For each CRUD operation, we'll:

1. Write a failing test
2. Implement the code to make it pass
3. Refactor if needed

| Section | What You'll Learn |
| - | - |
| [01 - Project Scaffold](https://docs.formae.ai/plugin-development/tutorial/01-scaffold.md) | Initialize the plugin project structure |
| [02 - Resource Schema](https://docs.formae.ai/plugin-development/tutorial/02-schema.md) | Define the File resource in PKL |
| [03 - Target Configuration](https://docs.formae.ai/plugin-development/tutorial/03-target.md) | Configure SFTP connection settings |
| [04 - Plugin Configuration](https://docs.formae.ai/plugin-development/tutorial/04-configuration.md) | Implement RateLimit and LabelConfig |
| [05 - Create](https://docs.formae.ai/plugin-development/tutorial/05-create.md) | Implement file creation (async pattern) |
| [06 - Read](https://docs.formae.ai/plugin-development/tutorial/06-read.md) | Implement file reading (NotFound semantics) |
| [07 - Update](https://docs.formae.ai/plugin-development/tutorial/07-update.md) | Implement file updates |
| [08 - Delete](https://docs.formae.ai/plugin-development/tutorial/08-delete.md) | Implement file deletion (NotFound semantics) |
| [09 - List](https://docs.formae.ai/plugin-development/tutorial/09-list.md) | Implement List for file discovery |
| [10 - Error Handling](https://docs.formae.ai/plugin-development/tutorial/10-error-handling.md) | OperationErrorCode patterns |
| [11 - Conformance tests](https://docs.formae.ai/plugin-development/tutorial/11-conformance.md) | Conformance tests against the formae agent |
| [12 - CI Setup](https://docs.formae.ai/plugin-development/tutorial/12-ci-setup.md) | Setting up CI/CD pipelines |
| [13 - Local Testing](https://docs.formae.ai/plugin-development/tutorial/13-local-testing.md) | Testing plugins locally |
| [14 - Observability](https://docs.formae.ai/plugin-development/tutorial/14-observability.md) | Logging and metrics for plugins |
| [15 - Real-world plugins](https://docs.formae.ai/plugin-development/tutorial/15-real-world-plugins.md) | Examples from production plugins |

## The asyncsftp library

SFTP operations are inherently synchronous (blocking I/O), but formae's plugin SDK supports an async pattern for long-running operations. We provide an `asyncsftp` library that wraps the standard SFTP client with an async interface:

```go theme={"languages":{"custom":["/languages/pkl.json"]}}
// Start an upload (returns immediately)
opID, err := client.StartUpload(path, content)

// Poll for completion
status, err := client.GetStatus(opID)
if status.State == asyncsftp.StateCompleted {
    // Operation finished
}
```

This pattern is common when integrating with infrastructure APIs. The library handles the complexity internally, so your plugin code just uses the clean async interface.

## Source code

The complete source code for this tutorial is available at:

**[github.com/platform-engineering-labs/formae-plugin-sftp](https://github.com/platform-engineering-labs/formae-plugin-sftp)**

***

Ready? Let's start with [01 - Project Scaffold](https://docs.formae.ai/plugin-development/tutorial/01-scaffold.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.