This document covers WebAssembly plugin development for Pocket. For information about built-in nodes and the overall plugin architecture, see:
- Plugin System Overview - Complete plugin architecture
- Node Types Reference - All 14 built-in node types
The Pocket plugin system allows extending the workflow engine with custom nodes written in any language that can compile to WebAssembly.
Pocket plugins are WebAssembly modules that extend the workflow engine with custom functionality. Plugins can:
- Add new node types for data processing
- Integrate with external services
- Implement custom business logic
- Transform and validate data
- Language Agnostic: Write plugins in TypeScript, Rust, Go, or any language that compiles to WASM
- Sandboxed Execution: Plugins run in isolated environments with controlled permissions
- Type Safe: Define schemas for inputs, outputs, and configuration
- Lifecycle Integration: Follows Pocket's Prep/Exec/Post pattern
my-plugin/
├── manifest.yaml # Plugin metadata and configuration
├── plugin.wasm # Compiled WebAssembly binary
├── src/ # Source code (language-specific)
└── README.md # Plugin documentation
The manifest describes the plugin and its capabilities:
name: my-plugin
version: 1.0.0
description: My custom plugin for Pocket
author: Your Name
license: MIT
runtime: wasm
binary: plugin.wasm
nodes:
- type: process-data
category: transform
description: Process and transform data
configSchema:
type: object
properties:
mode:
type: string
enum: ["fast", "accurate"]
default: "fast"
inputSchema:
type: object
properties:
data:
type: array
required: ["data"]
outputSchema:
type: object
properties:
result:
type: array
stats:
type: object
permissions:
memory: 10MB
timeout: 5s
requirements:
pocket: ">=1.0.0"Plugins follow Pocket's three-phase lifecycle:
- Prep Phase: Validate inputs, load configuration, prepare state
- Exec Phase: Execute core business logic (pure function)
- Post Phase: Process results, update state, determine routing
class MyNode extends PluginNode {
async prep(input: Input, config: Config, store: Store) {
// Validate and prepare data
return preparedData;
}
async exec(prepData: any, config: Config) {
// Core logic - no side effects
return result;
}
async post(input: Input, prepData: any, result: any, config: Config, store: Store) {
// Post-processing and routing
return { output: result, next: "success" };
}
}-
Install Pocket:
go install github.com/agentstation/pocket/cmd/pocket@latest
-
Choose your development language and install its toolchain:
- TypeScript: Node.js and Javy
- Rust: Rust toolchain with wasm32-wasi target
- Go: Go 1.21+ or TinyGo
-
Clone an example plugin:
cp -r $POCKET_ROOT/plugin/examples/typescript/sentiment-analyzer my-plugin cd my-plugin
-
Modify the plugin:
- Edit
src/index.ts(or equivalent) - Update
manifest.yaml - Implement your logic
- Edit
-
Build the plugin:
make build
-
Install locally:
pocket plugins install . -
Use in a workflow:
nodes: - name: my-processor type: my-node-type config: option: value
-
Set up project:
npm init -y npm install @pocket/plugin-sdk npm install -D typescript @shopify/javy
-
Create plugin:
import { Plugin, PluginNode, initializePlugin } from '@pocket/plugin-sdk'; class MyNode extends PluginNode<Input, Output, Config> { readonly type = 'my-node'; readonly category = 'custom'; readonly description = 'My custom node'; async prep(input: Input, config: Config, store: Store) { // Preparation logic return { processedInput: input }; } async exec(prepData: any, config: Config) { // Core processing return { result: process(prepData) }; } async post(input: Input, prepData: any, result: Output, config: Config, store: Store) { // Post-processing return { output: result, next: 'done' }; } } const plugin = new Plugin({ name: 'my-plugin', version: '1.0.0', nodes: [] }); plugin.register(new MyNode()); initializePlugin(plugin);
-
Build to WASM:
npm run build javy compile dist/plugin.js -o plugin.wasm
-
Set up project:
cargo init --lib # Add to Cargo.toml: # [lib] # crate-type = ["cdylib"]
-
Implement plugin:
use serde::{Deserialize, Serialize}; #[no_mangle] pub extern "C" fn alloc(size: usize) -> *mut u8 { // Memory allocation } #[no_mangle] pub extern "C" fn call(ptr: *const u8, len: usize, out_ptr: *mut u8, out_len: usize) -> usize { // Handle plugin calls }
-
Build:
cargo build --release --target wasm32-wasi
-
Create plugin:
//go:build wasm package main //export call func call(ptr uint32, size uint32, outPtr uint32, outSize uint32) uint32 { // Handle plugin calls } func main() { // Required for WASM }
-
Build:
tinygo build -o plugin.wasm -target wasi main.go # or GOOS=wasip1 GOARCH=wasm go build -o plugin.wasm main.go
The TypeScript SDK provides:
- Base classes for plugins and nodes
- Type definitions for all interfaces
- Memory management utilities
- Helper functions
Key components:
// Plugin class
class Plugin {
constructor(metadata: Metadata);
register(node: PluginNode): void;
}
// Base node class
abstract class PluginNode<TInput, TOutput, TConfig> {
abstract prep(input: TInput, config: TConfig, store: Store): Promise<any>;
abstract exec(prepData: any, config: TConfig): Promise<TOutput>;
abstract post(...): Promise<{ output: TOutput; next: string }>;
}
// Store interface
interface Store {
get(key: string): any;
set(key: string, value: any): void;
delete(key: string): boolean;
}WASM plugins must manage memory carefully:
// Allocate memory for data transfer
export function __pocket_alloc(size: number): number {
// Return pointer to allocated memory
}
// Free allocated memory
export function __pocket_free(ptr: number, size: number): void {
// Free the memory
}Plugins run in a sandboxed WebAssembly environment with:
- Memory Isolation: Each plugin has its own memory space
- No Network Access: Plugins cannot make network requests
- Limited Filesystem: No direct filesystem access
- Resource Limits: Configurable memory and execution time limits
Define permissions in the manifest:
permissions:
memory: 10MB # Maximum memory allocation
timeout: 5s # Maximum execution time
env: [] # Allowed environment variables
filesystem: [] # Allowed filesystem paths (future)- Validate All Inputs: Never trust external data
- Handle Errors Gracefully: Return proper error responses
- Respect Resource Limits: Design for constrained environments
- No Side Effects in Exec: Keep exec phase pure
- Document Security Considerations: Note any security implications
Plugin management is integrated into the main pocket CLI:
pocket plugins list- List installed pluginspocket plugins install <path>- Install a pluginpocket plugins remove <name>- Remove a pluginpocket plugins info <name>- Show plugin detailspocket plugins validate <path>- Validate a plugin
Analyzes text sentiment with configurable thresholds:
class SentimentAnalyzerNode extends PluginNode {
async exec(prepData: any, config: Config) {
const { words } = prepData;
const positiveCount = words.filter(w => positiveWords.includes(w)).length;
const negativeCount = words.filter(w => negativeWords.includes(w)).length;
const score = (positiveCount - negativeCount) / words.length;
const sentiment = score > config.threshold ? 'positive' :
score < -config.threshold ? 'negative' : 'neutral';
return { sentiment, score, confidence: Math.abs(score) };
}
}Counts words with stop word filtering:
fn handle_exec(request: &Request) -> Response {
let words: Vec<String> = cleaned_text
.split_whitespace()
.filter(|w| !config.stop_words.contains(w))
.collect();
let output = WordCounterOutput {
total_words: words.len(),
unique_words: word_frequencies.len(),
word_frequencies,
average_word_length: total_length as f64 / words.len() as f64,
};
Response { success: true, output: serde_json::to_value(output).unwrap() }
}Transforms JSON data with various operations:
func flattenJSON(data interface{}, params map[string]interface{}) (interface{}, int) {
result := make(map[string]interface{})
changes := 0
flattenHelper(data, "", separator, result, &changes)
return result, changes
}-
"Plugin not found"
- Check installation path:
~/.pocket/plugins/ - Verify manifest.yaml exists
- Check installation path:
-
"Memory limit exceeded"
- Increase limit in manifest.yaml
- Optimize memory usage
-
"Timeout exceeded"
- Increase timeout in permissions
- Optimize algorithm
-
"Invalid WASM module"
- Verify compilation target (wasm32-wasi)
- Check for missing exports
- Add logging to each phase
- Test with
pocket plugins validate - Start with simple logic, add complexity gradually
- Use the example plugins as reference
To contribute plugins:
- Follow the plugin structure
- Include comprehensive tests
- Document all configuration options
- Add examples in manifest.yaml
- Submit PR with plugin in
plugin/community/