Skip to content
Cogitate
Go back

AI Agents in Business Processes: Temporal, FSMs, and MCP

Updated:
| Björn Roberg, GPT-5.1

Position: durable workflows pair naturally with agentic workflows.

Put together, you get:

This post walks through:


Where Durable + Agentic Workflows Shine

These are business areas where “smart but fragile” agents become “smart and production-safe” once wrapped in durable orchestration.

High-Fit Business Areas

AreaAgent Does…Durable Workflow Does…
Customer onboarding & KYCUnderstand docs, ask for missing info, choose checksTrack all steps, retry APIs, enforce SLAs and approvals
Loan / credit underwritingInterpret financials, edge cases, draft rationaleOrchestrate bureaus, risk models, audit logs, notifications
Claims processingRead narratives/photos, propose coverage decisionsCoordinate intake → adjuster → documents → payout
Order-to-cash / quote-to-orderConfigure quotes, negotiate constraintsRoute approvals, contract flow, provisioning, invoicing
IT / HR service desksTriage, root-cause reasoning, run automated fixesManage SLAs, escalations, multi-team handoffs
Marketing & sales cadencesPersonalize outreach, adapt per responsesHandle timing, throttling, channel sequencing, logging
Procurement & vendor mgmtCompare bids, summarize contractsRun RFx stages, approvals, onboarding, renewals
Compliance workflowsInterpret regs, draft policies, review exceptionsEnforce required steps, evidence retention, sign-offs
Non-diagnostic health adminCoordinate benefits Q&A, reminders, educationManage multi-visit journeys, pre-auth, scheduling, follow-ups
Account management / CSDraft QBRs, suggest plays, interpret signalsRun multi-quarter plans, task orchestration, renewals

Pattern: anywhere you have multi-step, cross-system processes with human nuance, this pairing is strong.


FSMs: Putting Guardrails Around Agents

A core challenge with agents is that they’re “too free-form.” FSMs are a simple, powerful way to constrain behavior without killing flexibility. For a deeper guarantee — where the type system itself enforces state transitions — see Linear Types for Agent Safety.

Basic Idea

Think of it as:

Toy FSM for an Agentic Workflow

type State =
  | "CollectRequirements"
  | "Disambiguate"
  | "Plan"
  | "ExecuteTools"
  | "Summarize"
  | "Escalate";

interface Context {
  userInputs: string[];
  plan?: string;
  toolsRun: number;
  errors: string[];
  readyToExecute: boolean;
  done: boolean;
}

function transition(state: State, ctx: Context): State {
  switch (state) {
    case "CollectRequirements":
      return ctx.readyToExecute ? "Plan" : "CollectRequirements";

    case "Plan":
      return ctx.plan ? "ExecuteTools" : "Escalate";

    case "ExecuteTools":
      if (ctx.done) return "Summarize";
      if (ctx.errors.length > 2) return "Escalate";
      return "ExecuteTools";

    case "Summarize":
      return "Summarize";

    case "Disambiguate":
    case "Escalate":
      return state;
  }
}

The LLM’s job is to update Context (e.g., set readyToExecute, fill plan, mark done). The FSM decides what’s allowed next.


FSMs + MCP Servers: Structured Tool Use

MCP servers provide tooling backends (APIs, DB access, services). An FSM can:

Example:

StateAllowed MCP CapabilitiesRequired Before Transition
CollectRequirementsNone (chat only)Mandatory fields present (email, accountId, goal)
PlanRead-only MCP tools (search, knowledge base, schemas)Plan text + a list of tool calls with arguments
ExecuteToolsFull MCP access for this domainEither done=true or maxSteps reached
SummarizeRead-only history + notification toolsAt least one tool run, or explicit “no-op” explanation
EscalateTicketing / human handoff toolsEscalation reason + relevant context bundle

This yields a more enforceable contract between your agent and your infrastructure.


Implementing This in Production Workflows

The architecture above is engine-agnostic, but real implementations vary. Here’s how the pieces map onto Temporal, the most popular durable workflow engine for agentic systems.

Temporal Fundamentals

Temporal provides:

Mapping the Architecture to Temporal

Workflow = FSM + Orchestration

Your FSM transitions and context live inside a Temporal Workflow. The workflow handles durability; the FSM handles where the agent should be at each step.

// Your agentic workflow in Temporal
async function agenticWorkflow(input: WorkflowInput): Promise<WorkflowResult> {
  // Initialize FSM state and context
  let state: WorkflowState = "CollectRequirements";
  let context: Context = {
    userInputs: [],
    plan: null,
    toolsRun: 0,
    errors: [],
    readyToExecute: false,
    done: false,
  };

  // Main loop: keep going until done
  while (!context.done && state !== "Escalate" && state !== "Summarize") {
    // FSM transition
    const nextState = transition(state, context);

    // Call agent as an activity (this is where LLM invocation happens)
    const agentResult = await activities.invokeAgent({
      state: nextState,
      context,
      allowedTools: toolsForState(nextState),
      systemPrompt: promptForState(nextState),
    });

    // Validate tool calls against allowed tools for this state
    for (const toolCall of agentResult.toolCalls || []) {
      const allowed = toolsForState(nextState);
      if (!allowed.includes(toolCall.name)) {
        // Constraint violation: log and escalate if repeated
        context.errors.push(
          `Tool ${toolCall.name} not allowed in state ${nextState}`
        );
        if (context.errors.length > 2) {
          state = "Escalate";
          break;
        }
        continue;
      }
    }

    // Run tools (as activity, so failures are retried)
    if (agentResult.toolCalls && agentResult.toolCalls.length > 0) {
      try {
        const toolResults = await activities.runTools({
          toolCalls: agentResult.toolCalls,
          mimeType: "application/json",
        });

        context.toolsRun += toolResults.length;
        context.errors = [];  // Reset errors on success
      } catch (error) {
        context.errors.push(error.message);
        // Decision: retry in same state, or escalate?
        if (context.errors.length > 2) {
          state = "Escalate";
          break;
        }
      }
    }

    // Update context from agent result
    context = {
      ...context,
      userInputs: [...context.userInputs, agentResult.reasoning],
      plan: agentResult.plan || context.plan,
      readyToExecute: agentResult.readyToExecute ?? context.readyToExecute,
      done: agentResult.done ?? context.done,
    };

    // Transition to next state
    state = nextState;

    // Guard: prevent infinite loops
    if (context.toolsRun > 50) {
      state = "Escalate";
    }
  }

  // Final step based on terminal state
  if (state === "Summarize") {
    const summary = await activities.invokeAgent({
      state: "Summarize",
      context,
      allowedTools: ["notificationTools"],
      systemPrompt: promptForState("Summarize"),
    });
    return { status: "completed", context, summary };
  } else if (state === "Escalate") {
    const escalation = await activities.escalate({
      context,
      reason: context.errors.at(-1) || "max steps reached",
    });
    return { status: "escalated", context, escalationId: escalation.id };
  }

  return { status: "unknown", context };
}

Activities = Tool Execution + Retries

Each external action is an activity. Temporal automatically retries on failure:

// Activity: invoke the LLM
const invokeAgent = async (input: {
  state: WorkflowState;
  context: Context;
  allowedTools: string[];
  systemPrompt: string;
}): Promise<AgentResult> => {
  const client = new Anthropic();

  // Build tool descriptions from allowed list
  const toolDefs = buildToolDefinitions(input.allowedTools);

  const response = await client.messages.create({
    model: "claude-3-5-sonnet-20241022",
    max_tokens: 2000,
    system: input.systemPrompt,
    tools: toolDefs,
    messages: [
      {
        role: "user",
        content: formatContextForAgent(input.state, input.context),
      },
    ],
  });

  return parseAgentResponse(response);
};

// Activity: run tool calls (with retries per tool)
const runTools = async (input: {
  toolCalls: ToolCall[];
  mimeType: string;
}): Promise<ToolResult[]> => {
  const results: ToolResult[] = [];

  for (const call of input.toolCalls) {
    // Each tool call gets its own retry policy in Temporal UI
    const result = await executeTool(call.name, call.input);
    results.push({
      toolName: call.name,
      success: !result.error,
      output: result.error ? null : result.output,
      error: result.error?.message || null,
      duration: result.duration,
    });
  }

  return results;
};

// Activity: escalate to human
const escalate = async (input: {
  context: Context;
  reason: string;
}): Promise<{ id: string }> => {
  const ticket = await createTicket({
    type: "agentic_escalation",
    reason: input.reason,
    contextSnapshot: input.context,
    timestamp: new Date(),
  });

  // Optionally notify a human
  await notifyEscalation(ticket.id);

  return { id: ticket.id };
};

Observability & Debugging with Temporal

Temporal’s built-in observability is excellent:

  1. Workflow History: Every state transition, activity call, and result is logged

    • View in Temporal UI: state machine flows, inputs, outputs
    • Query: “show me all workflows that hit Escalate in CollectRequirements”
  2. Activity Retries: Temporal tracks each retry

    • See which activities fail repeatedly
    • Tune retry policies per activity/state
  3. Metrics: Native support for Prometheus, Datadog, etc.

    • Workflow duration per state
    • Tool success rates
    • Error rates by type
  4. Debugging: Replay failed workflows

    • Re-run from any point without side effects
    • Test fixes before pushing to production

Example Temporal Configuration

// Register workflow and activities
const client = new WorkflowClient();
const worker = await Worker.create({
  workflowsPath: require.resolve("./workflows"),
  activitiesPath: require.resolve("./activities"),
  connection,
  taskQueue: "agentic-workflows",
});

await worker.run();

// Start a workflow instance
async function startAgenticWorkflow(userId: string, goal: string) {
  const result = await client.workflow.execute(agenticWorkflow, {
    args: [{ userId, goal }],
    taskQueue: "agentic-workflows",
    workflowId: `agentic-${userId}-${Date.now()}`,
    // Retry policy: if entire workflow fails, retry for 24 hours
    retry: {
      initialInterval: "1m",
      maximumInterval: "10m",
      maximumAttempts: 100,  // 24h worth of retries
    },
  });

  return result;
}

// Query a running workflow
async function getWorkflowStatus(workflowId: string) {
  const workflow = client.getWorkflowHandle(workflowId);
  const state = await workflow.query(getState);  // Custom query
  return state;  // { state: "ExecuteTools", context: {...} }
}

// Send a signal (e.g., user message)
async function sendUserMessage(workflowId: string, message: string) {
  const workflow = client.getWorkflowHandle(workflowId);
  await workflow.signal(handleUserInput, { message });
}

When Temporal Shines

Multi-step orchestration across days/weeks (onboarding, claims, KYC) ✓ Guaranteed durability (crash-safe, audit trail) ✓ Observability (UI shows FSM state, tool calls, retries) ✓ Coordination (signals, timeouts, escalations built-in) ✓ At-scale (1000s of concurrent workflows)

Low-latency (workflow min ~100ms per step) ✗ Simple synchronous calls (overkill for single-step tasks)

Alternative: Step Functions (AWS) or DIY

If you’re AWS-first, Step Functions gives you similar FSM + durability but with a visual JSON definition. If you want total control, a simple queue + worker pattern with persistent state works too, though you’ll re-implement retry logic.

For most LLM-powered workflows at scale, Temporal is the sweet spot: battle-tested, observable, and specifically designed for exactly this pattern.


Combining It All with Durable Workflows

Durable workflows give you reliability over time; FSMs give you command and control; agents/MCP give you semantics and capabilities. The epistemic and moral frameworks behind those semantics are explored in Building Smarter AI Agents With Ideas From Philosophy.

Execution Loop Sketch

  1. Load workflow instance

    • Current FSM state
    • Context (user inputs, history, plan, tool results)
  2. Decide next state

    • Use FSM transition rules to pick:
      • Next state (and allowed tools for that state)
  3. Invoke agent + MCP tools

    • Call the LLM with:
      • System prompt describing current state + allowed actions/tools
      • Conversation history and context
    • If needed, call MCP tools the LLM selected.
  4. Update state & persist

    • Update context from LLM + tool results (e.g., readyToExecute, done, errors[]).
    • Compute next FSM state.
    • Persist again; schedule next “tick” or finish.
  5. Repeat until terminal state (Summarize or Escalate).

Pseudo-workflow tick (durable workflow engine handles retries, timeouts, recovery):

async function workflowTick(instanceId: string) {
  const { state, context } = await loadInstance(instanceId);

  const nextState = transition(state, context);           // FSM step

  const llmResult = await callAgent({
    state: nextState,
    context,
    allowedTools: toolsForState(nextState),
  });

  const { updatedContext, toolCalls } = await runTools(llmResult, context);

  await saveInstance(instanceId, {
    state: nextState,
    context: updatedContext,
  });

  if (!updatedContext.done && nextState !== "Summarize") {
    await scheduleNextTick(instanceId);
  }
}

Practical Ways to Go Further

Here are concrete next steps to turn these ideas into something real and robust.

1. Start with a Single, Narrow Use Case

Pick one process that is:

Examples:

Implement:

2. Make States and Actions Observable

Log:

This makes it easy to:

3. Explicitly Define Contracts per State

For each state, write down:

This can live as:

4. Close the Loop with Metrics

Track at least:

Then:

5. Gradually Increase Autonomy and Scope

Once the initial flow is stable:

Always keep:


Share this post on:

Previous Post
Markov chains and LLMs - hybrid architectures for smarter agents
Next Post
Context Management for AI Agents: Why MINDMAP Changes Everything