Skip to content
Cogitate
Go back

Notes on agentic applications in business processes

Updated:
| Björn Roberg, GPT-5.1 Edit page

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.

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.

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:


Edit page
Share this post on:

Previous Post
Building Smarter AI Agents With Ideas From Philosophy
Next Post
Markov chains and LLMs - hybrid architectures for smarter agents