runcor Documentation

Complete reference for the runcor AI runtime engine.

Overview

runcor is a runtime engine that sits between your application and foundation models. It handles model routing, retries, memory, cost tracking, policy enforcement, quality evaluation, external system connections, and business value analysis — so your code stays focused on business logic.

Your AI Tools          (thin — just workflow logic)
       |
     runcor             (routing, memory, cost, policy, agents, eval, scheduling, telemetry)
       |
Foundation Models      (Anthropic, OpenAI, Google)
External Systems       (Gmail, Slack, databases, APIs)

What runcor provides

Getting Started

Installation

bashnpm install runcor

Minimal example

typescriptimport { createEngine } from 'runcor';

const engine = await createEngine();

engine.register('hello', async (ctx) => {
  const response = await ctx.model.complete({
    prompt: 'Say hello in one sentence.',
  });
  return response.text;
});

const exec = await engine.trigger('hello', {
  idempotencyKey: 'hello-001',
});

With configuration

Create a runcor.yaml in your project root:

yamlproviders:
  - type: anthropic
    apiKey: ${ANTHROPIC_API_KEY}
    models: [claude-sonnet-4-20250514]
typescriptimport { createEngine } from 'runcor';

const engine = await createEngine(); // auto-loads runcor.yaml

engine.register('morning-brief', async (ctx) => {
  const brief = await ctx.model.complete({
    prompt: 'Summarize today\'s priorities.',
  });
  await ctx.memory.user.set('last-brief', brief.text);
  return brief.text;
});

await engine.trigger('morning-brief', {
  idempotencyKey: 'brief-2026-03-07',
  userId: 'alice',
});

Core Concepts

Executions

Every flow trigger creates an execution that moves through a state machine:

queued → running → complete
                 → failed
         running → waiting → running (resumed)
         running → retrying → running
queued/running/waiting → failed (via cancel)

Each execution has:

Concurrency control

The engine limits how many executions run simultaneously. Excess triggers are queued in FIFO order and dispatched as slots open.

typescriptconst engine = await createEngine({
  concurrency: 10,      // max 10 running at once (default: 100)
  drainTimeout: 15000,  // wait 15s for in-flight work on shutdown
});

Idempotency

Every trigger requires an idempotencyKey. If you trigger the same key twice, the second call returns the existing execution instead of creating a duplicate.

typescript// First call creates the execution
await engine.trigger('my-flow', { idempotencyKey: 'order-123' });

// Second call returns the same execution
await engine.trigger('my-flow', { idempotencyKey: 'order-123' });

Engine API

Creating an engine

typescriptimport { createEngine } from 'runcor';

// Minimal (in-memory, no providers)
const engine = await createEngine();

// With config object
const engine = await createEngine({
  model: { providers: [{ provider: myProvider, priority: 1 }] },
  concurrency: 50,
  drainTimeout: 10000,
});

// From runcor.yaml (auto-detected)
const engine = await createEngine();

Flow management

typescript// Register a flow
engine.register('my-flow', handler, {
  timeout: 60000,         // 60s max execution time
  maxRetries: 3,          // retry on transient failures
  description: 'Does X',  // shown in MCP server and HTTP API
  inputSchema: { ... },   // JSON Schema for input validation
});

// Unregister
engine.unregister('my-flow');

// List registered flows
const flows = engine.listFlows();
// [{ name: 'my-flow', config: { timeout: 60000, ... } }]

Execution control

typescript// Trigger
const exec = await engine.trigger('my-flow', {
  idempotencyKey: 'unique-key',
  input: { data: 'value' },
  userId: 'alice',
  sessionId: 'session-1',
  tenantId: 'tenant-a',
  metadata: { source: 'api' },
});

// Get execution status
const status = await engine.getExecution(exec.id);

// List executions
const all = await engine.list();
const running = await engine.list({ state: 'running' });
const byFlow = await engine.list({ flowName: 'my-flow' });

// Cancel
await engine.cancel(exec.id, 'No longer needed');

// Resume a waiting execution
await engine.resume(exec.id, { approved: true });

// Replay a completed execution
const replay = await engine.replay(exec.id);

// Delete a terminal execution
await engine.deleteExecution(exec.id);

// List waiting executions
const waiting = await engine.listWaiting('my-flow', { userId: 'alice' });

Events

The engine extends EventEmitter. Subscribe to lifecycle events:

typescriptengine.on('execution:state_change', (event) => {
  console.log(`${event.executionId}: ${event.from} → ${event.to}`);
});

engine.on('execution:complete', (event) => {
  console.log(`Done: ${event.result}`);
});

engine.on('cost:budget_exceeded', (event) => {
  console.log(`Budget exceeded: ${event.scope}`);
});

Available events:

EventPayload
ready{}
shutdown{}
execution:state_change{ executionId, from, to }
execution:complete{ executionId, result }
provider:health_change{ provider, state }
cost:request{ executionId, provider, model, cost, tokens }
cost:budget_warning{ scope, current, limit }
cost:budget_exceeded{ scope, current, limit }
policy:violation{ rule, flowName, reason }
policy:warning{ guardrail, flowName, reason }
policy:rate_limited{ name, scope }
eval:score{ executionId, evaluator, scores }
eval:complete{ executionId, overallScore, confidence }
eval:flagged{ executionId, reason }
adapter:connected{ name }
adapter:disconnected{ name }
adapter:error{ name, error }
adapter:tools_discovered{ name, tools }
adapter:tool_call{ adapter, tool, result }
flow:registered{ name }
flow:unregistered{ name }
scheduler:trigger{ flowName }
scheduler:skip{ flowName, reason }
scheduler:registered{ flowName, cron }
scheduler:removed{ flowName }
model:validation_retry{ executionId, error }
discernment:signal{ signal }
discernment:recommendation{ recommendation }
discernment:cycle{ report }

Shutdown

typescriptawait engine.shutdown(); // waits for drainTimeout, then force-fails stragglers

Status and capabilities

typescriptengine.getStatus();
// 'initializing' | 'ready' | 'shutting_down' | 'stopped'

engine.getCapabilities();
// { cost: true, evaluation: true, adapters: false, discernment: true, scheduler: true }

engine.getProviderHealth();
// [{ name: 'anthropic', healthState: 'healthy', priority: 1, costPerToken: { ... } }]

Flow Handlers

A flow handler is an async function that receives an ExecutionContext:

typescriptasync function myHandler(ctx: ExecutionContext): Promise<unknown> {
  // ctx.executionId   — unique execution ID
  // ctx.input         — data passed via trigger
  // ctx.model         — model completion API
  // ctx.memory        — scoped memory (tool, user, session)
  // ctx.cost          — cost tracking for this execution
  // ctx.telemetry     — OpenTelemetry integration
  // ctx.tools         — MCP adapter tools (if configured)
  // ctx.resumeData    — data from resume() (if resumed from waiting)

  const result = await ctx.model.complete({ prompt: 'Hello' });
  return result.text;
}

engine.register('my-flow', myHandler);

Flow configuration

typescriptengine.register('my-flow', handler, {
  timeout: 60000,              // max execution time (ms), default: 30000
  maxRetries: 5,               // retry attempts on transient failure, default: 3
  baseRetryDelay: 2000,        // initial backoff (ms), default: 1000
  maxRetryDelay: 60000,        // backoff cap (ms), default: 30000
  waitTimeout: 3600000,        // max time in waiting state (ms), default: 0 (indefinite)
  description: 'My flow',      // for MCP server and HTTP API
  inputSchema: { type: 'object', properties: { query: { type: 'string' } } },
  schedule: '0 8 * * *',       // cron expression
  timezone: 'America/New_York', // IANA timezone for schedule
  objective: 'customer-support', // primary business objective
  secondaryObjectives: ['cost-optimization'],
  expectedCadence: 'daily',
  purpose: 'Automated customer support triage',
  budget: { limit: 1.00, enforcement: 'hard' },
});

Model Routing

Providers

runcor supports four built-in providers:

typescriptimport { AnthropicProvider, OpenAIProvider, GoogleProvider, MockProvider } from 'runcor';

const engine = await createEngine({
  model: {
    providers: [
      { provider: new AnthropicProvider({ apiKey: '...' }), priority: 1 },
      { provider: new OpenAIProvider({ apiKey: '...' }), priority: 2 },
      { provider: new GoogleProvider({ apiKey: '...' }), priority: 3 },
    ],
    strategy: 'priority',
  },
});

Or configure via runcor.yaml:

yamlproviders:
  - type: anthropic
    apiKey: ${ANTHROPIC_API_KEY}
    priority: 1
    models: [claude-sonnet-4-20250514]
  - type: openai
    apiKey: ${OPENAI_API_KEY}
    priority: 2
    models: [gpt-4o]
  - type: google
    apiKey: ${GOOGLE_API_KEY}
    priority: 3
    models: [gemini-2.0-flash]

Routing strategies

StrategyBehavior
priorityUse highest-priority provider first, fall back on failure (default)
round-robinDistribute requests across providers evenly
lowest-costRoute to the cheapest provider by token cost

Circuit breaker

Each provider has a circuit breaker that tracks failures:

yamlrouting:
  strategy: priority
  maxFallbackAttempts: 3
  failureThreshold: 5     # failures before circuit opens
  cooldownMs: 30000        # time before retry (ms)

Model requests

typescriptconst response = await ctx.model.complete({
  prompt: 'Summarize this document.',
  model: 'claude-sonnet-4-20250514',   // optional: specific model
  maxTokens: 1000,                     // optional: max response tokens
  temperature: 0.7,                    // optional: 0-1
  provider: 'anthropic',              // optional: pin to specific provider
  systemPrompt: 'You are a helpful assistant.',
  messages: [                          // optional: multi-turn conversation
    { role: 'user', content: 'Hello' },
    { role: 'assistant', content: 'Hi!' },
    { role: 'user', content: 'Tell me more.' },
  ],
  tools: [                             // optional: tool definitions
    { name: 'search', description: '...', inputSchema: { ... } },
  ],
  responseFormat: { type: 'object', properties: { ... } }, // optional: JSON Schema
});

// Response
response.text;        // completion text
response.model;       // model used
response.provider;    // provider used
response.usage;       // { promptTokens, completionTokens }
response.toolCalls;   // tool call requests (if any)
response.parsed;      // parsed JSON (if responseFormat was a schema)

Streaming

typescriptconst stream = ctx.model.stream({
  prompt: 'Write a story.',
});

for await (const chunk of stream) {
  if (chunk.type === 'text_delta') {
    process.stdout.write(chunk.text);
  }
}

Cost Tracking

Budget configuration

typescriptconst engine = await createEngine({
  cost: {
    budgets: {
      perRequest: { limit: 0.50, enforcement: 'hard' },
      perUser: { limit: 10.00, enforcement: 'hard', window: { type: 'daily' } },
      perFlow: { limit: 50.00, enforcement: 'soft' },
      global: { limit: 1000.00, enforcement: 'hard', window: { type: 'monthly' } },
    },
    warningThreshold: 0.8,  // emit warning at 80% of budget
  },
});

Enforcement modes:

Window types: hourly, daily, monthly, custom (with durationMs), none (cumulative)

Accessing cost in flows

typescriptengine.register('my-flow', async (ctx) => {
  const result = await ctx.model.complete({ prompt: '...' });

  console.log(ctx.cost.executionTotal);  // cumulative cost of this execution
  console.log(ctx.cost.requestCount);    // number of model requests made
});

Querying the ledger

typescriptconst ledger = engine.getCostLedger();

const total = ledger.getTotal({ userId: 'alice' });
const entries = ledger.query({
  flowName: 'morning-brief',
  startTime: new Date('2026-03-01'),
  endTime: new Date('2026-03-07'),
});

Scoped Memory

Three memory scopes with different lifetimes:

ScopeLifetimeNamespace
toolPersistent per flowShared across all executions of the same flow
userPersistent per userShared across all flows for the same userId
sessionEphemeral per executionScoped to a single execution (gone after completion)

Usage

typescriptengine.register('my-flow', async (ctx) => {
  // Tool memory — persistent per flow
  const counter = (await ctx.memory.tool.get<number>('call-count')) ?? 0;
  await ctx.memory.tool.set('call-count', counter + 1);

  // User memory — persistent per user
  await ctx.memory.user.set('preferences', { theme: 'dark' });
  const prefs = await ctx.memory.user.get('preferences');

  // Session memory — ephemeral
  await ctx.memory.session.set('temp-token', 'abc123');

  // TTL support (seconds)
  await ctx.memory.tool.set('cache-key', data, 300); // expires in 5 minutes

  // List keys
  const keys = await ctx.memory.tool.list();

  // Delete
  await ctx.memory.tool.delete('old-key');
});

Wait / Resume / Replay

Pausing for external input

typescriptimport { createWaitSignal } from 'runcor';

engine.register('approval-flow', async (ctx) => {
  if (!ctx.resumeData) {
    // First run: pause and wait for approval
    return createWaitSignal({
      reason: 'Awaiting manager approval',
      expectedResumeBy: new Date(Date.now() + 86400000), // 24h
      waitData: { requestId: 'req-123' },
    });
  }

  // Resumed with approval data
  const { approved, comments } = ctx.resumeData as { approved: boolean; comments: string };
  if (approved) {
    return 'Request approved: ' + comments;
  }
  return 'Request denied';
});

// Trigger — execution pauses in "waiting" state
const exec = await engine.trigger('approval-flow', { idempotencyKey: 'approval-001' });

// Later — resume with data
await engine.resume(exec.id, { approved: true, comments: 'Looks good' });

Replaying executions

Replay creates a new execution from a completed one, using the same input:

typescriptconst original = await engine.trigger('my-flow', { idempotencyKey: 'key-1', input: { x: 1 } });
// ... execution completes ...

const replay = await engine.replay(original.id);
// New execution with same input, new ID

Agent Execution

Create autonomous agents that loop through model calls and tool invocations:

typescriptimport { createAgentHandler } from 'runcor';

engine.register('research-agent', createAgentHandler({
  systemPrompt: 'You are a research assistant. Use tools to find information.',
  tools: ['gmail.search', 'slack.search'],  // qualified tool names
  maxIterations: 10,
  timeoutMs: 120000,
  iterationBudget: 0.50,                    // max cost per iteration
  outputSchema: {                            // optional structured final answer
    type: 'object',
    properties: {
      summary: { type: 'string' },
      sources: { type: 'array', items: { type: 'string' } },
    },
    required: ['summary'],
  },
}));

const exec = await engine.trigger('research-agent', {
  idempotencyKey: 'research-001',
  input: { query: 'What were last quarter revenue numbers?' },
});

Agent result

typescriptinterface AgentResult {
  answer: unknown;           // final model response
  stopReason: 'completed' | 'max_iterations' | 'budget_exhausted' | 'timeout' | 'context_overflow';
  iterations: Array<{
    model: string;
    toolCalls: ToolCallRequest[];
    toolResults: ToolCallResult[];
    cost: number;
  }>;
  totalCost: number;
  totalTokens: { input: number; output: number };
  conversationLength: number;
}

Structured Output

Request JSON-conformant model responses with automatic validation and retry:

typescriptengine.register('extract-data', async (ctx) => {
  const response = await ctx.model.complete({
    prompt: 'Extract the person\'s name, age, and status from: "Alice is 30 and active."',
    responseFormat: {
      type: 'object',
      properties: {
        name: { type: 'string' },
        age: { type: 'integer' },
        active: { type: 'boolean' },
      },
      required: ['name', 'age', 'active'],
    },
  });

  const data = response.parsed;
  // { name: 'Alice', age: 30, active: true }
});

If the model response fails schema validation, runcor automatically retries once with a hint. If the retry also fails, a ValidationError is thrown.

Policy & Guardrails

Policy rules

Gate operations based on flow name, user, tenant, or custom logic:

typescriptengine.addPolicy({
  name: 'block-expensive-flows',
  operations: ['trigger'],
  evaluate: (context) => {
    if (context.flowName === 'bulk-export' && context.userId !== 'admin') {
      return { action: 'deny', reason: 'Only admins can run bulk exports' };
    }
    return { action: 'allow', reason: null };
  },
});

Content guardrails

Inspect and filter input/output content:

typescriptengine.addGuardrail({
  name: 'no-pii',
  phase: 'output',
  mode: 'block',     // 'block' | 'warn' | 'transform'
  handler: async (content, context) => {
    if (containsPII(content)) {
      return { action: 'block', reason: 'Output contains PII' };
    }
    return { action: 'pass', reason: null };
  },
});

Rate limiting

typescriptengine.addRateLimit({
  name: 'per-user-limit',
  scope: 'user',       // 'user' | 'flow' | 'global'
  limit: 100,
  windowMs: 3600000,   // 1 hour
  behavior: 'reject',  // 'reject' | 'queue'
});

Access control

typescriptengine.setAccessPolicy({
  identity: '[email protected]',
  allowedOperations: ['trigger'],      // can only trigger, not cancel/resume/replay
  deniedFlows: ['admin-panel'],
});

Multi-tenant configuration

typescriptengine.setTenantConfig({
  tenantId: 'customer-a',
  allowedFlows: ['customer-a-report'],
  rateLimits: [{ name: 'tenant-a-limit', scope: 'global', limit: 50, windowMs: 86400000 }],
});

Evaluation & Quality

Registering evaluators

typescriptengine.addEvaluator({
  name: 'quality-check',
  flowNames: ['morning-brief'],    // null = all flows
  evaluate: (context) => {
    const output = String(context.output);
    return {
      scores: {
        length: Math.min(output.length / 500, 1.0),
        hasActionItems: output.includes('action') ? 1.0 : 0.0,
      },
      labels: output.length > 200 ? ['detailed'] : ['brief'],
      feedback: output.length < 50 ? 'Response too short' : null,
    };
  },
});

Built-in evaluators

typescriptimport { createLengthEvaluator, createFormatEvaluator, createKeywordEvaluator } from 'runcor';

engine.addEvaluator(createLengthEvaluator({ minLength: 50, maxLength: 2000 }));
engine.addEvaluator(createFormatEvaluator({ expectedFormat: 'json' }));
engine.addEvaluator(createKeywordEvaluator({ required: ['summary', 'recommendation'] }));

Evaluation results

typescriptconst evaluation = engine.getEvaluation(executionId);
// {
//   overallScore: 0.85,
//   confidence: 'high',        // 'high' | 'medium' | 'low'
//   aggregateScores: { length: 0.9, hasActionItems: 0.8 },
//   labels: ['detailed'],
// }

Human review flags

typescript// Auto-flagging (via config)
const engine = await createEngine({
  evaluation: { autoFlagScoreThreshold: 0.3 }, // flag scores below 0.3
});

// Manual flagging
await engine.flagExecution(executionId, 'Needs review — unusual output');

// Managing flags
const flags = engine.listFlags({ status: 'pending' });
engine.updateFlag(executionId, 'reviewed');
engine.updateFlag(executionId, 'resolved');

MCP Adapters

Connect to external systems via the Model Context Protocol:

Configuration

yamlconnections:
  - name: gmail
    preset: gmail
    url: ${GMAIL_MCP_URL}
  - name: slack
    preset: slack
    url: ${SLACK_MCP_URL}
  - name: calendar
    preset: calendar
    url: ${CALENDAR_MCP_URL}
  - name: custom-api
    transport: sse
    url: https://api.example.com/mcp
    headers:
      Authorization: "Bearer ${API_TOKEN}"

Programmatic usage

typescriptawait engine.addAdapter({
  name: 'my-api',
  transport: 'sse',
  url: 'https://api.example.com/mcp',
  timeoutMs: 30000,
  retryAttempts: 3,
});

// List available tools
const tools = engine.listAdapterTools();
// [{ qualifiedName: 'my-api.searchUsers', description: '...', inputSchema: { ... } }]

// Call a tool directly
const result = await engine.callAdapterTool('my-api.searchUsers', { query: 'alice' });

Using tools in flows

typescriptengine.register('email-summary', async (ctx) => {
  // Discover available tools
  const tools = ctx.tools.listTools({ adapter: 'gmail' });

  // Call a tool
  const emails = await ctx.tools.callTool('gmail.search', {
    query: 'is:unread after:today',
    maxResults: 10,
  });

  const response = await ctx.model.complete({
    prompt: `Summarize these emails: ${JSON.stringify(emails)}`,
  });

  return response.text;
});

MCP Server

Expose registered flows as MCP tools for other AI agents to discover and invoke:

typescriptconst engine = await createEngine({
  server: { name: 'my-app', version: '1.0.0' },
});

engine.register('my-flow', handler, {
  description: 'Does something useful',
  inputSchema: { type: 'object', properties: { query: { type: 'string' } } },
});

await engine.startServer();
// Other MCP clients can now discover and call 'my-flow'

await engine.stopServer();

Scheduled Flows

Trigger flows on a cron schedule:

typescriptengine.register('daily-report', handler, {
  schedule: '0 8 * * *',            // 8 AM daily
  timezone: 'America/New_York',
});

Or via configuration:

yamlscheduler:
  defaultTimezone: America/New_York
  flows:
    daily-report:
      cron: "0 8 * * *"
      timezone: America/New_York

Features:

Discernment

Portfolio-level analysis that determines whether your AI workflows deliver business value.

Configuration

yamldiscernment:
  enabled: true
  autonomy: recommend     # observe | recommend | advise | enforce
  schedule: daily

objectives:
  - name: customer-support
    description: "Reduce support ticket volume through automated responses"
  - name: operational-visibility
    description: "Leadership has daily visibility into business metrics"

Tagging flows to objectives

typescriptengine.register('support-triage', handler, {
  objective: 'customer-support',
  expectedCadence: 'hourly',
  purpose: 'Automated support ticket classification',
});

Running analysis

typescript// Manual cycle
const report = await engine.runDiscernmentCycle();

// Report includes:
// - System profile (flow stats, cost, quality, adapter health)
// - Signals (idle flows, cost outliers, quality decline, agent hard stops)
// - Recommendations (keep, optimize, merge, retire, investigate, escalate)
// - Objective summaries

// Get recommendations
const recs = engine.getRecommendations({ action: 'retire' });
engine.acknowledgeRecommendation(recs[0].id);

Autonomy levels

LevelBehavior
observeCollect signals only
recommendSignals + model-generated recommendations
adviseRecommendations require acknowledgment
enforceAuto-execute recommendations after grace period

Built-in heuristic checks

Custom heuristics

typescriptengine.addHeuristic({
  name: 'low-usage',
  check: (profile) => {
    if (profile.executionCount < 5 && profile.totalCost > 10) {
      return {
        id: crypto.randomUUID(),
        checkName: 'low-usage',
        target: profile.flowName,
        targetType: 'flow',
        severity: 'warning',
        evidence: { executions: profile.executionCount, cost: profile.totalCost },
        timestamp: new Date(),
      };
    }
    return null;
  },
});

HTTP Server

Starting the server

typescriptimport { createEngine, createServer } from 'runcor';

const engine = await createEngine();
engine.register('my-flow', handler);

const server = await createServer(engine, {
  port: 3000,
  hostname: '127.0.0.1',
  cors: { origin: 'https://myapp.com' },
});

// Or via CLI: runcor dev

Authentication

Set AUTH_TOKEN environment variable to enable Bearer token authentication on mutating endpoints (POST, DELETE, PUT, PATCH).

Endpoints

MethodPathDescription
GET/v1/healthEngine status, uptime, capabilities
GET/v1/flowsList registered flows
POST/v1/flows/:name/triggerTrigger a flow
GET/v1/executionsList executions (with state, flowName, limit, offset query params)
GET/v1/executions/:idGet execution by ID
GET/v1/executions/:id/detailGet execution with cost entries and evaluation
POST/v1/executions/:id/resumeResume a waiting execution
POST/v1/executions/:id/replayReplay a completed execution
POST/v1/executions/:id/cancelCancel execution
DELETE/v1/executions/:idDelete terminal execution
GET/v1/adaptersList adapters and tools
POST/v1/adapters/:name/tools/:toolCall adapter tool
GET/v1/eventsSSE event stream (with type, executionId filters)
GET/v1/providersProvider health and circuit breaker states
GET/v1/cost/summaryCost totals by flow, user, and provider
GET/v1/discernmentObjectives, latest report, recommendations
GET/v1/dashboardBuilt-in monitoring dashboard

Trigger example

bashcurl -X POST http://localhost:3000/v1/flows/morning-brief/trigger \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $AUTH_TOKEN" \
  -d '{"idempotencyKey": "brief-001", "userId": "alice"}'

SSE event stream

bashcurl -N http://localhost:3000/v1/events?type=execution,cost

CLI

bash# Scaffold a new project
runcor init my-project

# Start dev server with auto-reload
runcor dev
runcor dev --config path/to/runcor.yaml

# Trigger a flow
runcor trigger morning-brief --input '{"date": "2026-03-07"}'

# Check execution status
runcor status
runcor status <execution-id>

# Resume a waiting execution
runcor resume <execution-id>

Project scaffolding

runcor init [project-name] creates:

my-project/
  runcor.yaml          # configuration (project name in header)
  flows/
    hello.mjs          # example flow
  .env.example         # environment template
  package.json
  tsconfig.json

The project name can be passed as a positional argument or with --name. If omitted, the current directory name is used.

SQLite Persistence

Use SQLite for durable execution state that survives process restarts:

yamlstate:
  type: sqlite
  path: ./data/runcor.db

Or programmatically:

typescriptimport { SQLiteStateStore } from 'runcor';

const engine = await createEngine({
  state: {
    type: 'sqlite',
    path: './data/runcor.db',
    onOrphanedExecution: (exec) => 'fail',  // 'fail' | 'requeue' | 'ignore'
  },
});

Features:

Observability

OpenTelemetry integration

typescriptimport { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';

const tracerProvider = new NodeTracerProvider();
tracerProvider.register();

const engine = await createEngine({
  telemetry: {
    tracerProvider,
    serviceName: 'my-ai-app',
    serviceVersion: '1.0.0',
  },
});

Every execution produces:

Custom instrumentation in flows

typescriptengine.register('my-flow', async (ctx) => {
  ctx.telemetry.setAttribute('custom.input_length', String(ctx.input).length);
  ctx.telemetry.addEvent('processing_started', { step: 'analysis' });

  const result = await ctx.telemetry.startSpan('heavy-computation', async () => {
    return doHeavyWork();
  });

  return result;
});

Log handler

typescriptconst engine = await createEngine({
  telemetry: {
    logHandler: (entry) => {
      console.log(`[${entry.severityText}] ${entry.body}`, entry.attributes);
    },
  },
});

Dashboard

The HTTP server includes a built-in monitoring dashboard at /v1/dashboard:

The dashboard is a single self-contained HTML page with embedded CSS and JavaScript — no React, no bundler, no new dependencies. Panels gracefully hide when subsystems are disabled.

Configuration Reference

Complete runcor.yaml schema:

yaml# Engine settings
engine:
  concurrency: 100              # max concurrent executions
  drainTimeout: 10000           # shutdown drain time (ms)
  retentionPeriod: 3600         # keep terminal executions (seconds)

# Model providers
providers:
  - type: anthropic             # anthropic | openai | google | mock
    apiKey: ${ANTHROPIC_API_KEY}
    baseUrl: https://api.anthropic.com  # optional override
    priority: 1                 # lower = higher priority
    models: [claude-sonnet-4-20250514]
    costPerToken:
      input: 0.003
      output: 0.015

# Routing strategy
routing:
  strategy: priority            # priority | round-robin | lowest-cost
  maxFallbackAttempts: 2
  failureThreshold: 5           # circuit breaker threshold
  cooldownMs: 30000             # circuit breaker cooldown

# External connections (MCP adapters)
connections:
  - name: gmail
    preset: gmail               # gmail | slack | calendar
    url: ${GMAIL_MCP_URL}
  - name: custom-api
    transport: sse              # stdio | sse
    url: https://api.example.com/mcp
    headers:
      Authorization: "Bearer ${API_TOKEN}"

# Cost tracking
costs:
  warningThreshold: 0.8
  defaultTokenEstimate: 1000
  maxLedgerEntries: 100000
  budgets:
    perRequest:
      limit: 0.50
      enforcement: hard         # hard | soft | disabled
    perUser:
      limit: 10.00
      enforcement: hard
      window:
        type: daily             # hourly | daily | monthly | custom | none
    perFlow:
      limit: 50.00
      enforcement: soft
    global:
      limit: 1000.00
      enforcement: hard

# Telemetry
telemetry:
  serviceName: my-app
  serviceVersion: 1.0.0
  memorySpans: false

# Policy engine
policy:
  rateLimits:
    - name: per-user
      scope: user               # user | flow | global
      limit: 100
      windowMs: 3600000
      behavior: reject          # reject | queue
  accessPolicies:
    - identity: "*"
      allowedOperations: [trigger, resume]
  tenants:
    - tenantId: customer-a
      allowedFlows: [customer-a-flow]

# Evaluation
evaluation:
  evaluators:
    - type: length
      config:
        minLength: 50
        maxLength: 2000
  autoFlagScoreThreshold: 0.3

# MCP server
server:
  enabled: true
  name: my-app
  version: 1.0.0

# HTTP server
httpServer:
  enabled: true
  port: 3000
  hostname: 127.0.0.1

# Scheduler
scheduler:
  defaultTimezone: America/New_York
  flows:
    daily-report:
      cron: "0 8 * * *"
      timezone: America/New_York

# State persistence
state:
  type: sqlite                  # memory | sqlite
  path: ./data/runcor.db

# Discernment (business value analysis)
discernment:
  enabled: true
  autonomy: recommend           # observe | recommend | advise | enforce
  schedule: daily
  lookbackPeriod: 604800        # 7 days (seconds)
  gracePeriod: 86400            # 24 hours (seconds)

# Business objectives
objectives:
  - name: customer-support
    description: "Reduce support ticket volume"
  - name: operational-visibility
    description: "Daily business metric visibility"

Environment variable interpolation

yamlapiKey: ${ANTHROPIC_API_KEY}              # required — throws if not set
baseUrl: ${BASE_URL:-https://default.com} # optional — uses default value

Error Handling

All engine errors throw EngineError with a code property:

typescriptimport { EngineError } from 'runcor';

try {
  await engine.trigger('unknown-flow', { idempotencyKey: 'key' });
} catch (err) {
  if (err instanceof EngineError) {
    console.log(err.code);    // 'FLOW_NOT_FOUND'
    console.log(err.message); // 'Flow "unknown-flow" not found'
  }
}

Error codes

CodeWhen
ENGINE_NOT_READYEngine not initialized or already stopped
ENGINE_SHUTTING_DOWNOperation attempted during shutdown
FLOW_NOT_FOUNDTrigger/unregister with unknown flow name
DUPLICATE_FLOWRegistering a flow name that already exists
EXECUTION_NOT_FOUNDGet/resume/cancel/replay with unknown ID
INVALID_STATEResume non-waiting or cancel completed execution
MISSING_IDEMPOTENCY_KEYTrigger without idempotencyKey
POLICY_DENIEDPolicy rule blocked the operation
RATE_LIMITEDRate limit exceeded
BUDGET_EXCEEDEDHard budget limit hit
VALIDATION_FAILEDStructured output failed schema validation
CONFIG_INVALIDInvalid runcor.yaml
CONFIG_NOT_FOUNDruncor.yaml not found
INVALID_SCHEDULEBad cron expression
ADAPTER_NOT_FOUNDUnknown adapter name
ADAPTER_NOT_CONNECTEDAdapter not in connected state
TOOL_NOT_FOUNDUnknown tool qualified name

Specialized errors

typescriptimport { BudgetExceededError, ValidationError, AllProvidersFailedError } from 'runcor';

// BudgetExceededError — thrown when hard budget is exceeded
// ValidationError — thrown when structured output fails schema
// AllProvidersFailedError — thrown when all fallback providers fail
// RetryableError — thrown by providers for transient failures (auto-retried)

Testing with MockProvider

The MockProvider lets you test flows without real API keys:

typescriptimport { createEngine, MockProvider } from 'runcor';

// Simple mock — returns a template string
const engine = await createEngine({
  model: { provider: new MockProvider('This is a mock response.') },
});

// Queue multiple responses
const mock = new MockProvider();
mock.queueResponses([
  { text: 'First response' },
  { text: 'Second response' },
  { toolCalls: [{ id: 'tc-1', name: 'search', arguments: { q: 'test' } }] },
]);

// Structured output — MockProvider generates schema-conformant data
const response = await ctx.model.complete({
  prompt: 'Extract data',
  responseFormat: {
    type: 'object',
    properties: {
      name: { type: 'string' },
      age: { type: 'integer' },
    },
  },
});
// response.parsed = { name: 'mock-string', age: 42 }

Project Structure

src/
├── index.ts                   # Public API exports
├── engine.ts                  # Core Runcor class
├── types.ts                   # Shared type definitions
├── execution.ts               # Execution state machine
├── execution-context.ts       # ExecutionContext builder
├── state-store.ts             # InMemoryStateStore
├── sqlite-state-store.ts      # SQLiteStateStore
├── wait-signal.ts             # Wait/resume primitives
├── model/
│   ├── router.ts              # Model routing engine
│   ├── strategies.ts          # Priority, round-robin, lowest-cost
│   ├── anthropic.ts           # Anthropic provider
│   ├── openai.ts              # OpenAI provider
│   ├── google.ts              # Google provider
│   ├── mock.ts                # Mock provider (testing)
│   └── validation.ts          # JSON Schema validation
├── memory/
│   ├── store.ts               # InMemoryStore
│   └── scoped.ts              # ScopedMemory (tool/user/session)
├── cost/
│   ├── tracker.ts             # CostTracker
│   ├── budget.ts              # Budget enforcement
│   └── ledger.ts              # InMemoryCostLedger
├── policy/
│   └── policy-engine.ts       # Rules, guardrails, rate limits, access
├── evaluation/
│   ├── evaluation-engine.ts   # Scoring and human review
│   └── built-in/              # Length, format, keyword evaluators
├── adapter/
│   ├── adapter-manager.ts     # MCP adapter lifecycle
│   └── reference/             # Gmail, Slack, Calendar presets
├── agent/
│   ├── handler.ts             # createAgentHandler()
│   └── types.ts               # AgentConfig, AgentResult
├── discernment/
│   ├── engine.ts              # DiscernmentEngine
│   ├── collector.ts           # Signal collection
│   ├── analyzer.ts            # Heuristic analysis
│   └── objectives.ts          # ObjectiveRegistry
├── scheduler/
│   └── cron-scheduler.ts      # Cron-based triggers
├── config/
│   ├── loader.ts              # YAML loading
│   ├── validator.ts           # Schema validation
│   ├── interpolator.ts        # Env var interpolation
│   └── mapper.ts              # Config → EngineConfig mapping
├── http/
│   ├── server.ts              # Hono HTTP server
│   ├── sse.ts                 # SSE event manager
│   ├── dashboard-html.ts      # Dashboard UI
│   └── routes/                # REST endpoints
├── server/
│   └── mcp-server-adapter.ts  # MCP server interface
├── cli/
│   ├── index.ts               # CLI dispatcher
│   └── commands/              # init, dev, trigger, status, resume
└── telemetry/
    └── instrumentation.ts     # OpenTelemetry integration

Requirements

License

MIT