Documentation
Overview

runners

Runners are headless, schedulable AI pipelines. While Agents are interactive (chat-based), Runners execute multi-step workflows without human interaction.

Concepts

  • Runner — a pipeline definition with ordered steps
  • Step — a stage in the pipeline (agent, computer, condition, or webhook)
  • Trigger — how a runner gets invoked (manual, API, cron, webhook, poll, bulk import, button)
  • Run — a single execution of a runner
  • Run Step — the result of executing one step in a run

Architecture

Trigger (cron / webhook / API / manual)
  → POST /api/runners/{id}/runs  (creates run, status='pending')
  → runRunnerPipeline(runId)     (lib/runner-engine.ts)
      for each step:
        1. Resolve input via JSONPath input_mapping
        2. Execute step (agent→Tensorlake, webhook→HTTP, condition→evaluate)
        3. Write result to runner_run_steps
        4. Merge output into run context
      Mark run completed

Key principle: Next.js orchestrates the pipeline. Tensorlake is called per agent step (same runTensorlake() as chat). The runtime stays stateless.

Runner Connections

Runners can connect directly to MCP servers, database integrations, custom tools, web tools, plans, and knowledge — the same resources available to agents. These runner-level connections serve as defaults for all steps in the pipeline.

How merging works

When a step references an agent (agent_id), both runner-level and agent-level configs are merged:

ResourceMerge strategy
MCP serversCombined; deduplicated by slug (agent wins on conflict)
IntegrationsCombined; agent overrides runner for same integration ID
ToolsCombined; deduplicated by name (agent wins on conflict)
Web configAgent overrides runner when agent has web config enabled

Steps without an agent_id use runner-level connections exclusively.

Attaching resources

// Attach an MCP server to a runner
await fj.attachRunnerMcp(runner.id, 'mcp-server-uuid');

// Attach a database integration
await fj.attachRunnerIntegration(runner.id, 'integration-uuid');

// Enable web tools
await fj.updateRunnerWebConfig(runner.id, {
  enabled: true,
  searchEnabled: true,
  researchEnabled: true,
});

// Enable plans
await fj.updateRunnerPlanConfig(runner.id, { enabled: true });

Runner-scoped knowledge

Upload knowledge docs scoped to a runner. These are automatically retrieved via similarity search during agent step execution.

curl -X POST https://api.flapjack.dev/api/knowledge/upload \
  -H "Authorization: Bearer fj_live_..." \
  -F "file=@product-catalog.txt" \
  -F "scopeType=runner" \
  -F "scopeId=RUNNER_ID" \
  -F "title=Product Catalog"

Runner-scoped knowledge is available to all steps in that runner, alongside org-scoped knowledge.

Connection tables

TablePurpose
runner_mcpsMCP server attachments (many-to-many)
runner_integrationsDatabase integration attachments
runner_toolsCustom tool attachments
runner_web_configWeb tools config (1:1)
runner_plan_configPlan config (1:1)

Step Types

agent — LLM execution via Tensorlake

  • Can reference an existing Agent (agent_id) or define inline config
  • When agent_id is set: loads the agent's preamble, model, tools, MCPs, integrations
  • preamble_override prepends to agent preamble (or IS the preamble for inline steps)
  • model_override overrides the model
  • output_schema instructs the LLM to return structured JSON

webhook — HTTP POST to external service

  • POSTs step input as JSON to webhook_url
  • Expects JSON response as step output

condition — Flow control

  • Evaluates condition_expr (JSONPath) against run context
  • If falsy: remaining steps are skipped

computer — Sandbox execution (Phase 2)

  • Executes code in a Tensorlake sandbox (ephemeral) or heyvm (persistent)
  • Same infrastructure as the Agent Computer feature — SandboxClient for Tensorlake, heyvm API for persistent
  • Useful for deterministic pipeline stages (image processing, data transforms) that don't need LLM reasoning

Input Mapping (JSONPath)

Steps receive input from the run context via input_mapping. The context accumulates as steps complete:

{
  "input": { "url": "https://example.com" },
  "steps": {
    "discovery": { "output": { "products": [...] }, "status": "completed" },
    "extraction": { "output": { "extracted": [...] }, "status": "completed" }
  }
}

Mapping example:

{
  "products": "$.steps.discovery.output.products",
  "url": "$.input.url"
}

Triggers

Manual / API

curl -X POST /api/runners/{id}/runs \
  -H "Authorization: Bearer fj_live_..." \
  -d '{"input": {"url": "https://example.com"}}'

Cron

A Vercel Cron job hits POST /api/cron/runners every minute. It evaluates cron expressions against last_fired_at.

Trigger config:

{ "kind": "cron", "cronExpression": "0 9 * * 1", "cronTimezone": "America/New_York" }

Webhook

Each webhook trigger gets a unique token. The inbound URL is public:

POST /api/runners/webhooks/{token}

Poll

Poll triggers watch external data sources via MCP tools and fire runs when new events are detected. The system tracks a cursor to avoid re-processing.

Trigger config:

{
  "kind": "poll",
  "pollConfig": {
    "provider": "github",
    "mcpServerId": "mcp-server-uuid",
    "template": "github_new_prs",
    "params": { "repo": "owner/repo" },
    "intervalMinutes": 5
  }
}
FieldDescription
providerData source key (e.g. github)
mcpServerIdMCP server to query
templatePoll template name (defines which MCP tools to call)
paramsTemplate-specific parameters
intervalMinutesHow often to check (default: 5)

The poll_cursor is tracked automatically per trigger and persisted between sweeps so only genuinely new events fire runs.

Bulk Import

curl -X POST /api/runners/{id}/runs/bulk \
  -H "Authorization: Bearer fj_live_..." \
  -d '{"items": [{"input": {"url": "..."}}, {"input": {"url": "..."}}]}'

Action Buttons (Button Trigger)

  • Hosted HTML button: GET /api/runners/{id}/button
  • Embeddable script: <script src="/api/runners/{id}/embed.js"></script>

SDK

import { FlapjackClient } from '@flapjack/sdk';

const fj = new FlapjackClient({ apiKey: 'fj_live_...' });

// Create a runner
const runner = await fj.createRunner({
  name: 'Product Discovery',
  inputSchema: { type: 'object', properties: { url: { type: 'string' } } },
});

// Add steps
await fj.addRunnerStep(runner.id, {
  name: 'discovery',
  kind: 'agent',
  orderIndex: 0,
  agentId: 'agent-uuid',
  inputMapping: { source: '$.input.url' },
  outputSchema: { type: 'object', properties: { products: { type: 'array' } } },
});

await fj.addRunnerStep(runner.id, {
  name: 'notify',
  kind: 'webhook',
  orderIndex: 1,
  webhookUrl: 'https://hooks.slack.com/...',
  inputMapping: { count: '$.steps.discovery.output.products.length' },
});

// Add a cron trigger
await fj.addRunnerTrigger(runner.id, {
  kind: 'cron',
  cronExpression: '0 9 * * 1',
});

// Trigger manually
const run = await fj.triggerRun(runner.id, { input: { url: 'https://example.com' } });

// Poll for completion
const result = await fj.getRun(runner.id, run.id);
console.log(result.status, result.output);

// --- Runner Connections ---

// Attach MCP servers directly to the runner
await fj.attachRunnerMcp(runner.id, 'mcp-server-uuid');

// Attach a database integration
await fj.attachRunnerIntegration(runner.id, 'integration-uuid');

// Enable web tools for the runner
await fj.updateRunnerWebConfig(runner.id, { enabled: true });

// Enable plans for the runner
await fj.updateRunnerPlanConfig(runner.id, { enabled: true });

// List attached MCPs
const mcps = await fj.listRunnerMcps(runner.id);

MCP Tools

All runner operations are available as MCP tools:

  • CRUD: flapjack_list_runners, flapjack_create_runner, flapjack_get_runner, flapjack_update_runner, flapjack_delete_runner
  • Steps: flapjack_add_runner_step, flapjack_update_runner_step, flapjack_remove_runner_step
  • Triggers: flapjack_add_runner_trigger, flapjack_update_runner_trigger, flapjack_remove_runner_trigger
  • Runs: flapjack_trigger_run, flapjack_trigger_run_bulk, flapjack_list_runs, flapjack_get_run, flapjack_cancel_run
  • Connections: flapjack_list_runner_mcps, flapjack_attach_runner_mcp, flapjack_detach_runner_mcp, flapjack_list_runner_integrations, flapjack_attach_runner_integration, flapjack_detach_runner_integration
  • Config: flapjack_get_runner_web_config, flapjack_update_runner_web_config, flapjack_get_runner_plan_config, flapjack_update_runner_plan_config

Database Tables

TablePurpose
runnersPipeline definitions
runner_stepsOrdered stages in a pipeline
runner_triggersTrigger configurations
runner_runsExecution history
runner_run_stepsPer-step execution log
runner_mcpsMCP server attachments
runner_integrationsDatabase integration attachments
runner_toolsCustom tool attachments
runner_web_configWeb tools config (1:1)
runner_plan_configPlan config (1:1)
runner_budget_configPer-runner budget enforcement config
runner_skillsSkill attachments

All tables use RLS with is_org_member() policies.

Idempotency

Runs support an idempotency_key. A unique index on (runner_id, idempotency_key) prevents duplicate runs:

await fj.triggerRun(runner.id, {
  input: { url: 'https://example.com/product-1' },
  idempotencyKey: 'https://example.com/product-1', // same key = same run
});

Budget Controls

Runner runs can be budget-capped to prevent runaway costs. Configure a default budget per runner, and each run inherits it.

Runner-Level Budget Config

curl -X PUT /api/runners/{id}/budget \
  -H "Authorization: Bearer fj_live_..." \
  -d '{"enabled": true, "budgetUsd": 5.00, "onExceed": "pause"}'
FieldTypeDescription
enabledbooleanWhether budget enforcement is active
budgetUsdnumber | nullMax USD per run (null = unlimited)
onExceed'pause' | 'fail'What happens when budget is exceeded
warningThreshold1numberFirst warning at this fraction (default: 0.80)
warningThreshold2numberSecond warning at this fraction (default: 0.95)

Per-Run Budget

Each RunnerRun has:

  • budget_usd — inherited from runner config at creation time
  • budget_on_exceed'pause' or 'fail'
  • budget_paused_at — timestamp when the run was paused

When a run exceeds its budget:

  • fail: The run is marked failed immediately
  • pause: The run enters paused_budget status and can be resumed

Resuming a Paused Run

curl -X POST /api/runners/{id}/runs/{runId}/resume \
  -H "Authorization: Bearer fj_live_..." \
  -d '{"additionalBudgetUsd": 2.00}'

The run resumes from its current_step. Optionally provide additionalBudgetUsd to increase the per-run budget before resuming.

Cost Analytics

Runner cost data is recorded per LLM call and attributed by model, enabling per-model cost breakdowns.

Per-Runner Analytics

curl https://api.flapjack.dev/api/runners/{id}/analytics?period=30d \
  -H "Authorization: Bearer fj_live_..."

Response (excerpt — also includes runner, period, summary):

{
  "cost_by_model": [
    { "model": "claude-sonnet-4-6", "cost_usd": 12.50 },
    { "model": "gpt-5.4", "cost_usd": 8.25 }
  ],
  "daily": [
    { "day": "2026-05-10", "total_runs": 5, "completed": 4, "failed": 1, "cost_usd": 2.10 }
  ]
}

Models are sorted by cost descending.

Org-Level Analytics

curl https://api.flapjack.dev/api/analytics?period=30d \
  -H "Authorization: Bearer fj_live_..."

Response includes by_model (excerpt — full response also contains period, summary, daily, by_agent, top_tools):

{
  "by_model": [
    { "model": "claude-sonnet-4-6", "unique_users": 5, "total_turns": 42, "estimated_cost_usd": 125.50 }
  ]
}

SDK

const analytics = await fj.getRunnerAnalytics(runner.id, { period: '30d' });
console.log(analytics.cost_by_model);

Self-Improvement (Phase 3)

The runners.learned_instructions field accumulates lessons from previous runs. These are prepended to every agent step's preamble, allowing the pipeline to improve over time.

Key Files

FilePurpose
supabase/migrations/026_runners.sqlCore runner database schema
supabase/migrations/028_runner_connections.sqlRunner connection tables (MCPs, integrations, tools, web, plan, knowledge)
lib/agent-config.tsShared agent config resolution
lib/runner-config.tsRunner config resolution (MCPs, integrations, tools, web, plan)
lib/runner-engine.tsPipeline execution orchestrator (merges runner + agent configs)
app/api/runners/API routes (CRUD, steps, triggers, runs, connections)
sdk/src/types.tsSDK types
sdk/src/client.tsSDK client methods
mcp/src/index.tsMCP tools
Docs last updated May 11, 2026