DEV Community

ZGI | AI Agent Platform
ZGI | AI Agent Platform

Posted on

When Agent Workflows Run for Hours: Designing for Failure, Recovery, and Inspection

#ai

A five-minute agent demo can assume that the process stays alive, the network cooperates, and every tool returns a clear result.

A five-hour workflow cannot.

Long-running agents wait on external APIs, process large files, call models repeatedly, trigger side effects, and sometimes pause for a human decision. During that time a worker can crash, a container can be replaced, a request can time out after the remote system has already succeeded, or a workflow can resume under a different prompt or tool version.

At that point, an agent workflow is no longer “a prompt with several tool calls.” It is a distributed workflow with probabilistic steps.

This article presents a generic production pattern for making those workflows recoverable and inspectable. It is a synthesis of durable-execution systems, workflow engines, observability standards, and published engineering cases. It is not a claim that any one agent platform provides every component described here.

Durable does not mean keeping one process alive

The central idea behind durable execution is simple:

Preserve the information required to continue outside the process that is doing the work.

Temporal persists workflow event history so a worker can reconstruct state after failure. LangGraph persistence stores graph-state checkpoints associated with a thread. AWS Step Functions redrive can continue eligible failed executions from unsuccessful steps while retaining successful step results.

The mechanisms differ, but the architectural lesson is consistent: memory in a worker is not durable state.

It also helps to separate three kinds of storage:

  1. Workflow state — the current node, attempts, decisions, deadlines, and checkpoint pointers for one run.
  2. Application memory — reusable user or organizational information that can survive across runs.
  3. Artifacts — large files, generated reports, tool outputs, and datasets stored separately and referenced by hash or URI.

Mixing these lifecycles makes recovery harder. A checkpoint should not need to serialize a two-gigabyte artifact, and long-term memory should not be treated as proof that a specific node completed successfully.

The failures you must model explicitly

“Failed” is too broad to be a useful workflow state. Different failures require different responses.

1. Transient dependency failure

Network interruptions, rate limits, temporary capacity problems, and service restarts may succeed on a later attempt. Retry them with a limit, exponential backoff, jitter, and a total retry budget.

2. Permanent or deterministic failure

Invalid arguments, missing permissions, incompatible schemas, and policy rejections will not improve with another identical request. Route them to a repair path, a human, or a terminal state.

3. Unknown side-effect outcome

This is the dangerous one. Your HTTP request timed out, but the remote service may already have sent the email, charged the card, created the ticket, or changed the database.

Do not classify this as ordinary failure. Use a first-class NEEDS_RECONCILIATION state and verify the external result before retrying.

4. Worker or sandbox loss

Containers are replaceable. The workflow identity, event history, and checkpoints must not be. Anthropic’s description of its managed-agent architecture makes a similar separation: a session is represented as an append-only event log, while harness and sandbox processes can be replaced.

5. Replay incompatibility

A checkpoint may outlive the code, prompt, model route, tool schema, or policy that created it. Resume against a pinned release, or perform an explicit migration. Silent upgrades during recovery turn an incident response into an untested deployment.

6. Retry storm

Retries add load to a system that may already be unhealthy. If every layer retries independently, traffic can multiply rapidly. Pick one retry-owning layer, add backoff and jitter, and use a circuit breaker when a dependency is likely to remain unavailable.

7. Zombie execution

An agent can loop indefinitely, wait forever for an approval, or continue spending tokens after the result has lost value. Every run needs a total deadline plus limits for model calls, tool calls, cost, iterations, and idle time.

8. Duplicate or concurrent resume

The same event can be delivered twice. Two people can approve simultaneously. A retry can race with a manual repair. Stable workflow IDs, optimistic locking, and idempotent resume tokens protect state transitions from duplication.

A minimum production architecture

A practical architecture does not need to be enormous, but it needs explicit boundaries:

Trigger / API
     |
     v
Durable Orchestrator
  - workflow_id
  - pinned release manifest
  - node states and attempts
  - deadlines and budgets
  - checkpoint pointers
     |
     +----> Node Workers / Tool Adapters
     |          |
     |          +----> External APIs / Databases
     |          +----> Artifact Store
     |          +----> Side-effect Ledger
     |
     +----> Human Inbox (wait / approve / resume)
     |
     +----> Trace, Log, and Evaluation Store
Enter fullscreen mode Exit fullscreen mode

The orchestrator owns state transitions. Workers execute bounded steps. The artifact store holds large outputs. The side-effect ledger records the intent, idempotency key, and external receipt for operations that change the world. The human inbox persists waiting states instead of holding an HTTP connection open. The trace store explains what happened across models, tools, queues, and services.

Give each node a real state machine

At minimum, use states that preserve operational meaning:

READY
  |
  v
RUNNING --------------------------> SUCCEEDED
  |
  +-- transient -----------------> RETRY_SCHEDULED --> READY
  +-- human required ------------> WAITING_HUMAN ----> READY
  +-- outcome unknown -----------> NEEDS_RECONCILIATION
  +-- permanent -----------------> FAILED_PERMANENT
  +-- rollback required ---------> COMPENSATING
                                      |
                                      +--> COMPENSATED / FAILED
Enter fullscreen mode Exit fullscreen mode

NEEDS_RECONCILIATION deserves special emphasis. “The client did not receive success” and “the operation did not happen” are different facts. AWS’s guidance on idempotent APIs recommends client-supplied request tokens and server-side records that associate a token with the original request. If the same token arrives with different parameters, that should be treated as an error rather than a new operation.

Idempotency belongs to the logical operation

An idempotency key should remain stable across attempts of the same logical action.

This is wrong:

key = f"{workflow_id}:{node_id}:{attempt_number}"
Enter fullscreen mode Exit fullscreen mode

Every retry gets a new key, so the downstream system sees a new operation.

Prefer:

key = stable_hash(workflow_id, node_id, logical_operation_id)
Enter fullscreen mode Exit fullscreen mode

Store the key before dispatching the side effect. Record the external receipt when it arrives. If the response is lost, query the downstream service or ledger using that same identity.

Do not assume a workflow framework removes this responsibility. Temporal notes that an Activity can run successfully and then be retried if the worker crashes before reporting completion. LangGraph warns that code before an interrupt may execute again when the node resumes. The application still needs safe side-effect semantics.

A simplified execution loop

The following pseudocode omits framework-specific details, but shows the responsibilities clearly:

async def run_workflow(workflow_id, request):
    state = store.load_or_create(
        workflow_id=workflow_id,
        request=request,
        release_manifest=current_release_manifest(),
    )

    while not state.is_terminal:
        enforce_deadline(state)
        enforce_cost_and_tool_budgets(state)

        node = planner.next_ready_node(state)

        if node.requires_human:
            token = issue_resume_token(
                workflow_id=workflow_id,
                node_id=node.id,
                state_version=state.version,
            )
            store.checkpoint(state.wait_for_human(node.id, token))
            return {"status": "waiting_human", "token": token}

        key = stable_hash(workflow_id, node.id, node.logical_operation_id)

        try:
            result = await execute_with_policy(
                node=node,
                state=state,
                idempotency_key=key,
                timeout=node.timeout,
                retry_if=is_transient_and_safe,
                backoff="exponential_with_jitter",
            )

            side_effect_ledger.record_receipt(key, result.receipt)
            state = state.apply_result(node.id, result)
            store.checkpoint(state)

        except UnknownSideEffectOutcome as error:
            store.checkpoint(
                state.needs_reconciliation(node.id, key, error)
            )
            return {"status": "needs_reconciliation"}

        except PermanentError as error:
            state = compensate_or_fail(state, node, error)
            store.checkpoint(state)

    return state.final_result
Enter fullscreen mode Exit fullscreen mode

Notice what is not left implicit: deadlines, budgets, release versions, human waits, idempotency, reconciliation, compensation, and checkpoint boundaries.

Human-in-the-loop is a durable state, not a modal dialog

Human review is often presented as a UI feature. Architecturally, it is a long-lived workflow state.

Pause before irreversible actions such as sending an external message, deleting data, moving money, changing production systems, or approving access. Persist the WAITING_HUMAN state. Bind the resume token to the workflow, node, and expected state version. Make repeated submissions idempotent. Provide expiration, rejection, cancellation, and escalation paths.

Most importantly, revalidate business conditions when the workflow resumes. Permission, price, inventory, risk, and policy may have changed while the agent was waiting.

Trace the decisions, not only the final answer

OpenTelemetry defines a span as a unit of work with timing, attributes, events, links, and status. That model maps well to agent nodes. A node-level record should include:

  • workflow, run, node, attempt, trace, and parent-span IDs;
  • workflow, prompt, model, tool, and knowledge-index versions;
  • input, output, state, and checkpoint references with hashes;
  • idempotency key, side-effect intent, receipt, and reconciliation state;
  • timeout, deadline, heartbeat, retry reason, and backoff;
  • human request, actor, decision, and timestamp;
  • error class, policy result, evaluation score, and final state.

Agent-specific tracing systems can add model generations, tool calls, handoffs, and guardrail events. Trace grading can then identify which step caused a regression. It should be treated as a debugging and evaluation aid, not as proof that the workflow is correct.

Where ZGI fits

We are building ZGI, an open-source, self-hostable Agent Runtime. It brings agents, visual workflows, knowledge, database connections, reusable Skills, model integrations, and tool execution into one workspace. The goal is to reduce repeated integration work for developers and make agent applications easier to assemble and operate.

ZGI is relevant here because production reliability spans the whole agent application, not only the model call. A shared runtime gives teams a place to connect the workflow, tools, knowledge, and human interactions that need to be inspected together.

This article’s checkpoint protocol, retry model, side-effect ledger, state machine, and tracing fields are a general reference architecture. They should not be interpreted as a statement that ZGI currently implements every mechanism exactly as shown. We prefer to keep that boundary explicit while building the project in the open.

The test that matters

The most useful question for a long-running agent is not “Can it complete the happy path?”

It is:

If the worker disappears immediately after step 12 changes an external system, can we determine what happened, avoid repeating the action, restore the correct versioned state, and continue safely?

If the answer is unclear, the workflow is not durable yet.

Long-running agent reliability is mostly the disciplined engineering of state, identity, side effects, time, and evidence. Models make the workflow intelligent. The runtime makes it operable.


References

Top comments (0)