Build

Flowchart as tool

Wrap a footprintjs FlowChart as one Agent tool. Compose existing multi-step procedures (decision evidence, branches, loops, pause/resume) into the LLM's tool surface — no flatten/restructure required.

A team has a working footprintjs flowchart for refund processing — validate → look-up-order → decide-eligibility → process-refund → notify, with decide() evidence captured at the eligibility step. The agent that needs to expose this as a tool shouldn't have to flatten it into 5 separate tools. flowchartAsTool wraps the existing chart as ONE tool the LLM can call. The flowchart's recorders, narrative, and pause/resume continue to work exactly as outside the agent.

What it does

import { flowChart } from 'footprintjs';
import { flowchartAsTool, Agent } from 'agentfootprint';

// Existing flowchart — defined once, shared by humans + agents
const refundChart = flowChart<RefundState>('RefundFlow', validateInput, 'validate')
  .addDeciderFunction('Eligibility', eligibilityDecider, 'eligibility')
    .addFunctionBranch('eligible', 'Process', processRefund)
    .addFunctionBranch('ineligible', 'Notify', notifyIneligible)
    .end()
  .build();

// One-line bridge to the Agent's tool surface
const refundTool = flowchartAsTool({
  name: 'process_refund',
  description: 'Process a refund. Validates, decides eligibility, processes or notifies.',
  inputSchema: {
    type: 'object',
    properties: {
      orderId: { type: 'string' },
      reason:  { type: 'string' },
    },
    required: ['orderId', 'reason'],
  },
  flowchart: refundChart,
});

const agent = Agent.create({ provider, model: 'claude-sonnet-4-5' })
  .system('You handle refund requests.')
  .tool(refundTool)
  .build();

What happens at runtime

When the LLM calls process_refund({ orderId, reason }):

  1. A fresh FlowChartExecutor(refundChart) is constructed (no cross-call state leakage).
  2. executor.run({ input: args, env: { signal: ctx.signal } }) runs — args land in scope.$getArgs(); the agent's abort signal propagates.
  3. The chart executes normally. Every footprintjs recorder you'd attach for human-only use (narrative, metrics, decide-evidence, etc.) continues to fire.
  4. executor.getSnapshot() is read; resultMapper(snapshot) (or the default JSON.stringify(snapshot.values)) returns the string the LLM sees as the tool result.
  5. If the chart's decide() evidence was captured, that evidence lives in the snapshot — resultMapper can extract specific commit artifacts to surface in the tool result.

Customizing the result

The default mapper is JSON.stringify(snapshot.values). Override with resultMapper:

const refundTool = flowchartAsTool({
  name: 'process_refund',
  description: '...',
  flowchart: refundChart,
  resultMapper: (snapshot) => {
    const v = snapshot.values as { refundId?: string; reason?: string };
    return JSON.stringify({
      ok: v.refundId !== undefined,
      refundId: v.refundId,
      explanation: v.reason ?? 'no explanation',
    });
  },
});

FlowchartToolSnapshot carries:

FieldWhat it holds
valuesFinal merged scope state (what every stage wrote)
narrativeCombined narrative entries (flow + data ops) for audit-style result shaping

If the mapper throws, the tool result is enveloped as [mapper-error: <reason>] so the LLM sees a useful diagnostic instead of an unhandled crash.

Pause inside a flowchart

If a stage uses addPausableFunction and pauses (waiting for a human approval, e.g.), the wrapped tool throws an Error with the checkpoint attached:

try {
  await agent.run({ message: 'refund O-100' });
} catch (e) {
  const err = e as Error & { checkpoint?: unknown };
  if (err.checkpoint) {
    // Persist the checkpoint; resume the inner flowchart later.
    // (Reconstruct the inner FlowChartExecutor and call
    //  executor.resume(checkpoint, humanInput) to continue.)
  }
}

Today: the pause surfaces as a thrown error so the consumer can persist the inner chart's checkpoint manually. An integrated path — where the agent's outer run pauses cleanly with the inner checkpoint folded into the agent's own checkpoint envelope — is not yet wired into flowchartAsTool. For agent-native human-in-the-loop, use askHuman / pauseHere inside a stage and the Agent's own pause/resume (RunCheckpointError + agent.resumeOnError(checkpoint)).

Why this matters

Without flowchartAsToolWith flowchartAsTool
Flatten N-stage flowchart into N tools the LLM can call independentlyWrap once; LLM calls one tool
LLM has to plan the right sequence each turnSequence is already encoded in the chart
Tool-level errors lose the chart's branch contextErrors propagate with the chart's full narrative + commitLog
Decision evidence (decide()) is invisible to the agentresultMapper can surface it directly
Pre-existing flowchart code requires rewriteExisting code unchanged

Composition with other primitives

flowchartAsTool produces a regular Tool — it composes with everything tool-side:

import { staticTools, gatedTools } from 'agentfootprint/tool-providers';
import { PermissionPolicy } from 'agentfootprint/security';

const refundTool   = flowchartAsTool({...});
const triageTool   = flowchartAsTool({...});
const policy       = PermissionPolicy.fromRoles({...}, 'support');

const provider = gatedTools(
  staticTools([refundTool, triageTool, lookupTool]),
  (name) => policy.isAllowed(name),
);

Or wrap inside a defineSkill for LLM-activated unlocking:

const refundSkill = defineSkill({
  id: 'refund',
  description: 'Refund processing capabilities',
  body: 'When the user asks to refund, call process_refund with the order id.',
  tools: [refundTool],  // The flowchart-backed tool
  autoActivate: 'currentSkill',  // Narrow the tool list to this skill's tools while it's the active skill
});

Anti-patterns

  • Don't put async LLM calls inside the wrapped flowchart without thinking about it. Each tool call is one inference step from the LLM's perspective; if the inner chart calls another LLM, that's a billable second inference the agent's cost tracker doesn't see. Use LLMCall or Agent runners for that — they integrate with the cost subsystem.
  • Don't share mutable state between calls. Each tool.execute() constructs a fresh executor; if you need persistence across invocations, use defineMemory or a side store.
  • Don't ignore the abort signal. Long-running flowcharts should honor ctx.signal so the agent's caller can cancel the whole run.

Next steps

  • Tools guide — the underlying Tool primitive
  • Tool providersgatedTools / skillScopedTools chains
  • Output schema — pair with outputSchema when the wrapped chart's result is a structured contract
  • footprintjs — the underlying flowchart pattern

On this page