Flank Seeds
Flank Seeds
Git-driven definitions for adapters and workflows. Synced to the sirloin database on every server startup.
How Seed Sync Works
On startup (server/entry.ts), flank reads all JSON files from seeds/adapters/ and seeds/workflows/ and upserts them via gRPC:
- New entity (not in DB) → created
- Existing, seed version higher than DB → updated
- Existing, seed version ≤ DB → skipped (DB is newer, e.g. edited via UI)
Sync is idempotent — safe to restart repeatedly.
Making Changes
- Edit the seed JSON file
- Bump
seedVersion(required — equal/lower versions are skipped) - Restart the flank server (or Docker container)
- Verify with
pnpm validate:seedsbefore committing
Validation
pnpm validate:seedsChecks all seed files for:
- Required fields present
- Valid connection/auth types
- Graph structure (no cycles, exactly one trigger, no disconnected nodes)
Adapter Seed Format
See adapters/adapter.schema.json for the full JSON Schema.
{ "id": "my-adapter", // Unique slug (lowercase, hyphens) "name": "My Adapter", // Display name "description": "What it does", // Optional "category": "inference", // inference | storage | ml | ai | legacy "seedVersion": 1, // Bump to push updates to DB
"connection": { "type": "rest", // rest | grpc | s3 "baseUrl": "https://api.example.com" },
"auth": { "type": "bearer", // bearer | api-key-header | basic | aws-sig-v4 | none "headerName": "Authorization", "valueTemplate": "Bearer {{secret:my-api-key}}" },
"operations": [ { "id": "generate", "name": "Generate Something", "description": "Calls the /generate endpoint", "inputSchema": { "type": "object", "properties": { "prompt": { "type": "string" } }, "required": ["prompt"] }, "request": { "method": "POST", "path": "/generate", "bodyTemplate": { "prompt": "{{input.prompt}}" } }, "outputMapping": { "taskId": "$.data.id" // JSONPath-like extraction from response }, "polling": { // Optional: for async operations "request": { "method": "GET", "path": "/tasks/{{taskId}}" }, "statusField": "$.status", "completedValues": ["completed"], "failedValues": ["failed"], "outputMapping": { "result": "$.data.output" }, "intervalMs": 2000, "maxAttempts": 120 }, "retry": { // Optional "maxRetries": 2, "backoffMs": 1000, "retryableStatusCodes": [429, 502, 503] } } ]}Secret References
Adapter auth and templates can reference secrets with {{secret:name}}. Secrets are stored encrypted in sirloin and managed via the flank UI (/secrets) or pnpm provision:secrets.
For local dev, secrets fall back to env vars: {{secret:wavespeed-api-key}} → FLANK_SECRET_WAVESPEED_API_KEY.
Workflow Seed Format
{ "name": "my-workflow", // Unique name (used as lookup key) "description": "What it does", "seedVersion": 1, // Bump to push updates
"inputSchema": { // JSON Schema for workflow inputs "type": "object", "properties": { "user_id": { "type": "string" } }, "required": ["user_id"] },
"graph": { "nodes": [ { "id": "trigger", "type": "core:trigger", // Format: "nodeType:kind" "position": { "x": 0, "y": 200 }, "config": {} }, { "id": "load-data", "type": "data:character", "position": { "x": 250, "y": 200 }, "config": { "characterId": "{{trigger.character_id}}" } }, { "id": "generate", "type": "adapter:wavespeed-seedream-v4:generate-image", "position": { "x": 500, "y": 200 }, "config": { "prompt": "{{nodes.load-data.description}}" } } ], "edges": [ { "id": "e1", "source": "trigger", "target": "load-data" }, { "id": "e2", "source": "load-data", "target": "generate" } ] }}Node Type Format
Node types in seeds use compound strings: "type:kind".
| Format | Type | Kind | Example |
|---|---|---|---|
core:trigger | core | trigger | Workflow entry point |
core:template | core | template | String template |
core:condition | core | condition | If/else branch |
core:fallback | core | fallback | Primary + fallback adapter |
data:character | data | character | Load character from sirloin |
adapter:wavespeed-seedream-v4:generate-image | adapter | wavespeed-seedream-v4:generate-image | Call adapter operation |
Template Syntax
Templates use {{...}} placeholders:
{{trigger.field}}— workflow input field{{nodes.nodeId.outputKey}}— output from a previous node{{secret:name}}— resolved secret value