Routine Execution
Routine Execution
For new workflows that must verify business completion, prefer the additive
verified routine runner. The legacy API below remains
supported. Default and generated prompts use the current store_set / store_get
tools. Nested map/fold/until instructions reach reused agents through the task
prompt, while their system instructions and shared tool manager remain intact.
Prompt overrides remain optional. See the offline store/map/fold example
and release report.
Execute tasks in dependency order with shared working memory and optional LLM quality reflection. Host-verified business completion uses the separate facade above.
Overview
The Routine Runner (executeRoutine()) takes a RoutineDefinition and executes it end-to-end:
- Creates an Agent with working memory + in-context memory enabled
- Runs tasks in dependency order (respecting
dependsOn) - Runs LLM self-reflection only when both
skipReflection: falseand nonempty completion criteria are configured; otherwise quality auto-passes - Clears conversation between tasks but preserves memory plugins — this is how tasks share data
- Retries failed tasks up to
maxAttempts - Returns a
RoutineExecutionwith status, progress, and per-task results
Task A → validate → clear conversation → Task B → validate → clear → Task C → done
↓ ↓
memory persists ──────────────→ memory persists
Quick Start
import {
Connector, ConnectorTools, Services, tools, Vendor, executeRoutine,
createRoutineDefinition,
} from 'oneringai';
// 1. Setup connector
Connector.create({
name: 'openai',
vendor: Vendor.OpenAI,
auth: { type: 'api_key', apiKey: process.env.OPENAI_API_KEY! },
});
Connector.create({
name: 'serper-main', serviceType: Services.Serper,
auth: { type: 'api_key', apiKey: process.env.SERPER_API_KEY! },
baseURL: 'https://google.serper.dev',
});
const searchTools = ConnectorTools.for('serper-main');
// 2. Define a routine
const routine = createRoutineDefinition({
name: 'Research Report',
description: 'Research a topic and write a report',
instructions: 'You are a research assistant. Be thorough and cite sources.',
tasks: [
{
name: 'Research',
description: 'Search for information about quantum computing advances in 2026',
expectedOutput: 'A summary of key findings stored in memory',
suggestedTools: ['serper-main_web_search', 'web_fetch'],
validation: {
skipReflection: false, // Reflection is opt-in; not a host completion proof.
completionCriteria: [
'At least 3 distinct sources were found',
'Key findings were stored in memory',
],
},
},
{
name: 'Write Report',
description: 'Write a structured report based on the research findings',
expectedOutput: 'A markdown report with introduction, findings, and conclusion',
dependsOn: ['Research'],
validation: {
skipReflection: false, // Reflection is opt-in; not a host completion proof.
completionCriteria: [
'Report has an introduction, findings section, and conclusion',
'Report cites specific sources from the research',
],
},
},
],
});
// 3. Execute
const execution = await executeRoutine({
definition: routine,
connector: 'openai',
model: 'gpt-4.1',
tools: [...searchTools, tools.webFetch],
onTaskComplete: (task) => console.log(`Done: ${task.name}`),
onTaskFailed: (task) => console.error(`Failed: ${task.name}`),
});
console.log(execution.status); // 'completed' | 'failed'
console.log(execution.progress); // 100
RoutineDefinition Structure
interface RoutineDefinition {
name: string; // Routine name
description: string; // Human-readable description (required)
instructions?: string; // System prompt for the agent
requiredTools?: string[]; // Tool names that must be available
requiredPlugins?: string[]; // Plugin names that must be registered
tasks: TaskInput[]; // Array of task definitions (TaskDefinition does not exist)
concurrency?: {
maxParallelTasks: number; // Required if concurrency is set; runner executes tasks sequentially regardless
strategy: 'fifo' | 'priority' | 'shortest-first';
failureMode?: 'fail-fast' | 'continue' | 'fail-all'; // Default: 'fail-fast'
};
}
Task Definition
Each task in a routine has:
interface TaskInput {
name: string; // Task name (used as ID)
description: string; // What the agent should do
expectedOutput?: string; // What success looks like
suggestedTools?: string[]; // Hint to the agent about useful tools
dependsOn?: string[]; // Task names this depends on
maxAttempts?: number; // Max retry attempts (default: 3)
validation?: {
completionCriteria?: string[]; // Criteria for LLM self-reflection
minCompletionScore?: number; // Minimum score 0-100 (default: 80)
skipReflection?: boolean; // Skip validation, auto-pass
};
}
Control Flow (Map / Fold / Until)
Tasks can use control flow to iterate over data. Three flow types are available:
map— Execute a sub-routine for each element in an array (from memory key)fold— Accumulate a result across array elements (likeArray.reduce)until— Repeat a sub-routine until a condition is met
All three support an optional iterationTimeoutMs to prevent infinite hangs:
const mapTask = {
name: 'Process Items',
description: 'Process each item from the list',
controlFlow: {
type: 'map',
source: '__items_list',
resultKey: '__processed_items',
maxIterations: 50,
iterationTimeoutMs: 120000, // 2 min per iteration
tasks: [
{ name: 'Process', description: 'Process the current item' },
],
},
};
When iterationTimeoutMs is set, each sub-execution is wrapped with Promise.race. If an iteration exceeds the timeout (or its sub-execution otherwise fails), the entire map/fold/until task fails immediately — there is no per-iteration failure mode or skip-and-continue; the loop does not proceed to remaining iterations.
Error Classification
The routine runner classifies errors as transient or permanent:
- Permanent errors (auth failures, context length exceeded, model not found, config errors) immediately fail the task without retrying
- Transient errors (network issues, rate limits, unknown errors) are retried up to
maxAttempts
This prevents wasting retries on errors that will never succeed.
ROUTINE_KEYS Constant
The ROUTINE_KEYS constant is exported from the core library and contains all well-known ICM/WM key names used internally by the routine framework:
import { ROUTINE_KEYS } from 'oneringai';
// ROUTINE_KEYS.PLAN → '__routine_plan'
// ROUTINE_KEYS.DEPS → '__routine_deps'
// ROUTINE_KEYS.DEP_RESULT_PREFIX → '__dep_result_'
// ROUTINE_KEYS.MAP_ITEM → '__map_item'
// ROUTINE_KEYS.MAP_INDEX → '__map_index'
// ROUTINE_KEYS.MAP_TOTAL → '__map_total'
// ROUTINE_KEYS.FOLD_ACCUMULATOR → '__fold_accumulator'
Useful for custom integrations that need to read or set routine state programmatically.
Task Dependencies and Ordering
Tasks execute in dependency order. Use dependsOn to create a DAG:
const routine = createRoutineDefinition({
name: 'Data Pipeline',
tasks: [
{ name: 'Fetch Data', description: '...' },
{ name: 'Clean Data', description: '...', dependsOn: ['Fetch Data'] },
{ name: 'Analyze', description: '...', dependsOn: ['Clean Data'] },
{ name: 'Visualize', description: '...', dependsOn: ['Analyze'] },
],
});
Tasks with no dependencies run first. A task only becomes eligible when all its dependencies are completed.
Memory as the Inter-Task Bridge
Between tasks, conversation history is cleared but memory plugins persist. This is how tasks share information:
-
In-Context Memory (
store_set("whiteboard", ...)): Small key results that subsequent tasks see immediately in context. No retrieval call needed.Task 1 calls: store_set({ store: "whiteboard", key: "api_endpoints", description: "...", value: "Found 3 endpoints: /users, /orders, /products" }) Task 2 sees this automatically in its context window -
Working Memory (
store_set("notes", ...)/store_get("notes", ...)): Larger data stored externally, retrieved on demand.Task 1 calls: store_set({ store: "notes", key: "raw_data", description: "Full API response", value: "...", tier: "findings" }) Task 2 calls: store_get({ store: "notes", key: "findings.raw_data" }) → gets the full response
The defaults use these unified tools. Read each notes source with its exact index key, including tier/collision suffixes; the index and keyless reads contain metadata only. Large routine dependencies and results spill to the findings tier, and internal control-flow resolution preserves exact-key precedence before checking that spill location. Routine cleanup removes its reserved tiered keys and preserves unrelated host notes. Map/fold iteration state requires the in-context memory plugin. Fold captures the last completed subtask output first; a whiteboard accumulator update is only a fallback when no task output exists.
Validation and Self-Reflection
After each task completes, the runner validates the output:
- LLM validation is opt-in: it only runs when the task sets
skipReflection: falseexplicitly AND provides non-emptycompletionCriteria - Otherwise (the default —
skipReflectionomitted/true, or nocompletionCriteria) the task auto-passes withcompletionScore: 100 - When enabled: calls
agent.runDirect()with the task criteria and agent's response - The validation LLM is asked to return
{ isComplete, completionScore, explanation }, but onlycompletionScore/explanationare used — the LLM's ownisCompleteis ignored - Task passes if
completionScore >= minCompletionScore(default 80)
const validation = {
validation: {
skipReflection: false, // Reflection is opt-in; not a host completion proof.
completionCriteria: [
'All API endpoints were documented',
'Error codes were listed for each endpoint',
'Authentication requirements were specified',
],
minCompletionScore: 85, // Strict: require 85+ score
},
};
Retry Logic
If validation fails and the task hasn't exhausted maxAttempts:
- The conversation is NOT cleared (agent builds on previous attempt)
- Task status returns to
in_progress(increments attempt counter) - Agent retries with the same prompt
If maxAttempts is exceeded, the task is marked failed.
Failure Modes
const routine = createRoutineDefinition({
name: 'Pipeline',
concurrency: {
maxParallelTasks: 1,
strategy: 'fifo',
failureMode: 'fail-fast', // Default: stop on first failure
// failureMode: 'continue', // Skip failed tasks, continue with independents
},
tasks: [],
});
| Mode | Behavior |
|---|---|
fail-fast |
Stop entire routine on first task failure |
continue |
Skip failed task, proceed to next independent task |
Custom Prompts
Override any prompt builder to customize agent behavior:
const execution = await executeRoutine({
definition: routine,
connector: 'openai',
model: 'gpt-4.1',
prompts: {
// Custom system prompt
system: (definition) => `You are ${definition.name}. Follow these rules...`,
// Custom task prompt
task: (task) => `
## ${task.name}
${task.description}
Remember to store results using store_set("whiteboard", key, { description, value }).
`,
// Custom validation prompt
validation: (task, context) => `
Did the agent complete "${task.name}"?
Criteria: ${task.validation?.completionCriteria?.join(', ')}
Response: ${context.responseText}
Return JSON: { "isComplete": boolean, "completionScore": number, "explanation": string }
`,
},
});
Callbacks and Progress Tracking
const execution = await executeRoutine({
definition: routine,
connector: 'openai',
model: 'gpt-4.1',
onTaskComplete: (task, execution) => {
console.log(`Task "${task.name}" completed`);
console.log(` Score: ${task.result?.validationScore}`);
console.log(` Progress: ${execution.progress}%`);
},
onTaskFailed: (task, execution) => {
console.error(`Task "${task.name}" failed after ${task.attempts} attempts`);
console.error(` Error: ${task.result?.error}`);
},
});
ExecuteRoutineOptions Reference
interface ExecuteRoutineOptions {
/** Routine definition to execute */
definition: RoutineDefinition;
/** Pre-created Agent instance. When provided, connector/model/tools are ignored.
* The agent is NOT destroyed after execution — caller manages its lifecycle. */
agent?: Agent;
/** Connector name — required when `agent` is not provided */
connector?: string;
/** Model ID — required when `agent` is not provided */
model?: string;
/** Additional tools — only used when creating a new agent (no `agent` provided) */
tools?: ToolFunction[];
/** Input parameter values for parameterized routines */
inputs?: Record<string, unknown>;
/** Hooks — applied to agent for the duration of routine execution */
hooks?: HookConfig;
/** Called when a task starts executing */
onTaskStarted?: (task: Task, execution: RoutineExecution) => void;
/** Called when a task completes successfully */
onTaskComplete?: (task: Task, execution: RoutineExecution) => void;
/** Called when a task fails */
onTaskFailed?: (task: Task, execution: RoutineExecution) => void;
/** Called after each validation attempt (whether pass or fail) */
onTaskValidation?: (task: Task, result: TaskValidationResult, execution: RoutineExecution) => void;
/** Configurable prompts (all have sensible defaults) */
prompts?: {
system?: (definition: RoutineDefinition) => string;
task?: (task: Task) => string;
validation?: (task: Task, context: ValidationContext) => string;
};
}
RoutineExecution Result
interface RoutineExecution {
id: string; // Unique execution ID
routineId: string; // From definition
status: 'pending' | 'running' | 'paused' | 'completed' | 'failed' | 'cancelled';
plan: Plan; // Tasks with statuses and results
progress: number; // 0-100 percentage
startedAt?: number; // Timestamp
completedAt?: number; // Timestamp
lastUpdatedAt: number; // Timestamp
error?: string; // Error message if failed
}
Each completed task has a result:
// Shape of Task.result (not a separately exported type)
interface TaskResult {
success: boolean;
output?: unknown; // Agent's response text (or control-flow result)
error?: string; // Error message if failed
validationScore?: number; // 0-100 from validation
validationExplanation?: string; // Why it passed/failed
}
Complete Example: Multi-Step Research Pipeline
import {
Connector, Vendor, Agent,
executeRoutine, createRoutineDefinition,
ConnectorTools, Services, tools,
} from 'oneringai';
// Setup connectors
Connector.create({
name: 'openai',
vendor: Vendor.OpenAI,
auth: { type: 'api_key', apiKey: process.env.OPENAI_API_KEY! },
});
Connector.create({
name: 'serper',
serviceType: Services.Serper,
auth: { type: 'api_key', apiKey: process.env.SERPER_API_KEY! },
baseURL: 'https://google.serper.dev',
});
// Define the routine
const routine = createRoutineDefinition({
name: 'Competitive Analysis',
description: 'Analyze competitors and produce a report',
instructions: 'You are a business analyst. Be thorough and data-driven.',
requiredTools: ['web_search'],
tasks: [
{
name: 'Identify Competitors',
description: 'Search for the top 5 competitors of Acme Corp in the cloud storage market',
expectedOutput: 'List of competitors stored in context',
suggestedTools: ['web_search'],
validation: {
skipReflection: false, // Reflection is opt-in; not a host completion proof.
completionCriteria: [
'At least 5 competitors were identified',
'Competitor names were stored using store_set("whiteboard", ...)',
],
},
},
{
name: 'Analyze Features',
description: 'For each competitor found, research their key features and pricing',
dependsOn: ['Identify Competitors'],
expectedOutput: 'Feature comparison data stored in memory',
suggestedTools: ['web_search', 'web_fetch'],
maxAttempts: 2,
validation: {
skipReflection: false, // Reflection is opt-in; not a host completion proof.
completionCriteria: [
'Features were researched for all identified competitors',
'Pricing information was found for at least 3 competitors',
'Data was stored in working memory',
],
minCompletionScore: 70, // More lenient — pricing may not always be public
},
},
{
name: 'Write Report',
description: 'Compile findings into a structured competitive analysis report',
dependsOn: ['Analyze Features'],
expectedOutput: 'A markdown report with executive summary, feature comparison table, and recommendations',
validation: {
skipReflection: false, // Reflection is opt-in; not a host completion proof.
completionCriteria: [
'Report has an executive summary',
'Report includes a feature comparison table',
'Report provides actionable recommendations',
],
},
},
],
});
// Execute with search tools
const searchTools = ConnectorTools.for('serper');
const execution = await executeRoutine({
definition: routine,
connector: 'openai',
model: 'gpt-4.1',
tools: [...searchTools, tools.webFetch],
onTaskComplete: (task, exec) => {
console.log(`[${exec.progress}%] ${task.name} completed (score: ${task.result?.validationScore})`);
},
onTaskFailed: (task) => {
console.error(`FAILED: ${task.name} — ${task.result?.error}`);
},
});
if (execution.status === 'completed') {
// Get the final report from the last task's output
const reportTask = execution.plan.tasks.find(t => t.name === 'Write Report');
console.log(reportTask?.result?.output);
} else {
console.error(`Routine failed: ${execution.error}`);
}
Best Practices
- Use
store_set("whiteboard", ...)for small shared state — task names, IDs, short summaries. These are always visible. - Use
store_set("notes", ...)for large data — full API responses, documents, raw data. Retrieved on demand viastore_get("notes", key). - Separate quality from completion — LLM criteria assess an answer; host verifiers must check actual artifacts and remote state.
- Set appropriate
minCompletionScore— use 80+ for strict validation, 60-70 for lenient. - Opt into reflection explicitly with
skipReflection: falseand criteria when needed; omission already auto-passes. - Keep
maxAttemptsreasonable — 2-3 for most tasks. Each retry costs LLM calls. - Use
continuefailure mode when tasks are independent and partial results are acceptable.
Routine Persistence
Save routine definitions to disk and load them later using FileRoutineDefinitionStorage (or implement IRoutineDefinitionStorage for custom backends like MongoDB).
import {
createFileRoutineDefinitionStorage,
createRoutineDefinition,
} from 'oneringai';
const storage = createFileRoutineDefinitionStorage();
// Save a routine
const routine = createRoutineDefinition({
name: 'Daily Report',
description: 'Generate a daily status report',
tags: ['daily', 'reports'],
author: 'alice',
tasks: [
{ name: 'Gather Data', description: 'Collect metrics from all sources' },
{ name: 'Write Report', description: 'Summarize findings', dependsOn: ['Gather Data'] },
],
});
await storage.save(undefined, routine); // undefined = default user
// Load by ID
const loaded = await storage.load(undefined, routine.id);
// List with filtering
const dailyRoutines = await storage.list(undefined, { tags: ['daily'] });
const searchResults = await storage.list(undefined, { search: 'report', limit: 10 });
// Delete
await storage.delete(undefined, routine.id);
Storage Path: ~/.oneringai/users/<userId>/routines/<id>.json (defaults to ~/.oneringai/users/default/routines/)
Multi-user isolation: Pass a userId to isolate routines per user. When userId is undefined, defaults to 'default'.
StorageRegistry integration:
import { StorageRegistry } from 'oneringai';
StorageRegistry.configure({
routineDefinitions: (ctx) => new MongoRoutineStorage(ctx?.userId),
});
Execution Recording
Persist full routine execution history — every step, task snapshot, and progress update — without manually wiring hooks and callbacks.
Types:
| Type | Purpose |
|---|---|
RoutineExecutionRecord |
Top-level persisted record (status, progress, tasks, steps, trigger info) |
RoutineTaskSnapshot |
Per-task snapshot (status, attempts, result, controlFlowType) |
RoutineExecutionStep |
Timestamped event (task.started, tool.call, llm.complete, etc.) |
RoutineTaskResult |
Task outcome (success, output, error, validationScore) |
Factory functions:
import {
createRoutineExecutionRecord,
createTaskSnapshots,
createExecutionRecorder,
} from 'oneringai';
// 1. Create the initial record from a definition
const record = createRoutineExecutionRecord(definition, 'openai', 'gpt-4.1', {
type: 'schedule',
source: 'daily-cron',
});
// 2. Insert into your storage backend
const execId = await storage.insert(userId, record);
// 3. Create recorder — returns ready-to-use hooks + callbacks
const recorder = createExecutionRecorder({
storage,
executionId: execId,
logPrefix: '[MyRoutine]',
maxTruncateLength: 500, // truncate tool args/results in steps
});
// 4. Wire into executeRoutine()
executeRoutine({
definition, agent, inputs,
hooks: recorder.hooks,
onTaskStarted: recorder.onTaskStarted,
onTaskComplete: recorder.onTaskComplete,
onTaskFailed: recorder.onTaskFailed,
onTaskValidation: recorder.onTaskValidation,
})
.then(exec => recorder.finalize(exec))
.catch(err => recorder.finalize(null, err));
What the recorder tracks:
| Hook/Callback | Step Type(s) |
|---|---|
before:llm |
llm.start |
after:llm |
llm.complete (with duration/tokens) |
before:tool |
tool.start |
after:tool |
tool.call (with args/result, truncated) |
after:execution |
iteration.complete |
pause:check |
heartbeat (lastActivityAt update) |
onTaskStarted |
task.started, control_flow.started |
onTaskComplete |
task.completed, control_flow.completed |
onTaskFailed |
task.failed |
onTaskValidation |
task.validation |
finalize |
execution.error (on failure), final status update |
Storage interface (IRoutineExecutionStorage):
interface IRoutineExecutionStorage {
insert(userId: string | undefined, record: RoutineExecutionRecord): Promise<string>;
update(id: string, updates: Partial<Pick<RoutineExecutionRecord, 'status' | 'progress' | 'error' | 'completedAt' | 'lastActivityAt'>>): Promise<void>;
pushStep(id: string, step: RoutineExecutionStep): Promise<void>;
updateTask(id: string, taskName: string, updates: Partial<RoutineTaskSnapshot>): Promise<void>;
load(id: string): Promise<RoutineExecutionRecord | null>;
list(userId: string | undefined, options?: { routineId?: string; status?: RoutineExecutionStatus; limit?: number; offset?: number }): Promise<RoutineExecutionRecord[]>;
hasRunning(userId: string | undefined, routineId: string): Promise<boolean>;
}
Implement this interface for your own storage backend (MongoDB, PostgreSQL, etc.), or use the library's built-in FileRoutineExecutionStorage (createFileRoutineExecutionStorage()), which persists records to ~/.oneringai/users/<userId>/routine-executions/<executionId>.json.
StorageRegistry integration:
StorageRegistry.configure({
routineExecutions: (ctx) => new MongoRoutineExecutionStorage(ctx?.userId),
});
Scheduling
Run routines on a timer using IScheduler. The built-in SimpleScheduler supports interval and one-time schedules:
import { SimpleScheduler } from 'oneringai';
const scheduler = new SimpleScheduler();
// Repeat every hour
scheduler.schedule('hourly-report', { intervalMs: 3600000 }, async () => {
await executeRoutine({ definition: dailyRoutine, agent, inputs });
});
// Run once at a specific time
scheduler.schedule('end-of-day', { once: endOfDayTimestamp }, async () => {
await executeRoutine({ definition: summaryRoutine, agent, inputs });
});
// Cancel a schedule
scheduler.cancel('hourly-report');
// Check if a schedule exists
scheduler.has('hourly-report'); // false
// Clean up all timers
scheduler.destroy();
ScheduleSpec options:
| Field | Type | Description |
|---|---|---|
intervalMs |
number |
Repeat every N milliseconds |
once |
number |
Fire once at this Unix timestamp (ms) |
cron |
string |
Cron expression (not supported by SimpleScheduler — use a cron library) |
timezone |
string |
IANA timezone for cron expressions |
SimpleScheduler is intentionally minimal. For cron support, implement IScheduler with a cron library (e.g., croner, node-cron):
import type { IScheduler, ScheduleHandle, ScheduleSpec } from 'oneringai';
import { Cron } from 'croner';
class CronScheduler implements IScheduler {
private jobs = new Map<string, Cron>();
private _isDestroyed = false;
schedule(id: string, spec: ScheduleSpec, callback: () => void | Promise<void>): ScheduleHandle {
if (spec.cron) {
const job = new Cron(spec.cron, { timezone: spec.timezone }, callback);
this.jobs.set(id, job);
return { id, cancel: () => this.cancel(id) };
}
throw new Error('CronScheduler only handles cron specs');
}
cancel(id: string) { this.jobs.get(id)?.stop(); this.jobs.delete(id); }
cancelAll() { for (const [id] of this.jobs) this.cancel(id); }
has(id: string) { return this.jobs.has(id); }
destroy() { this.cancelAll(); this._isDestroyed = true; }
get isDestroyed() { return this._isDestroyed; }
}
Event Triggers
Trigger routine execution from external events (webhooks, message queues, custom signals) using EventEmitterTrigger:
import { EventEmitterTrigger } from 'oneringai';
const trigger = new EventEmitterTrigger();
// Register a handler
const unsubscribe = trigger.on('new-order', async (payload) => {
const order = payload as { orderId: string; items: string[] };
await executeRoutine({
definition: orderProcessingRoutine,
agent,
inputs: { orderId: order.orderId, items: order.items },
});
});
// Emit from your webhook handler, queue consumer, etc.
app.post('/webhooks/orders', (req, res) => {
trigger.emit('new-order', req.body);
res.sendStatus(200);
});
// Unsubscribe a specific handler
unsubscribe();
// Clean up all listeners
trigger.destroy();
Key design: EventEmitterTrigger is intentionally simple — no ITriggerSource interface. For complex trigger systems (AWS SQS, RabbitMQ, Kafka), just call trigger.emit() from your consumer callback.