RiverbornBook Call

From Tutorial Agent to Workflow Graph: Migrating a Sales AI System on Google ADK

Engineering7 min read

How a multi-tenant sales agent system grew from a Google ADK hello-world into a 40-worker orchestrator, then got rebuilt on ADK 2.0 Workflow graphs: what broke, what held up, and what we'd tell any team making the same jump.

From Tutorial Agent to Workflow Graph: Migrating a Sales AI System on Google ADK

On day one, the repository was recognizably the Google ADK tutorial: Agent, InMemorySessionService, Runner, a Gemini 2.0 Flash model, and a couple of demo tools that said hello and checked the weather. Within days it stopped being a notebook and became a real service. FastAPI in front, per-org validation, a per-org session service, a CLI entry point. That's the arc this post traces: from tutorial agent to production sales system to a full architecture swap, and the ten months of real engineering in between.

The system is the agent layer for an outbound sales product: research accounts, draft email, run cadences, book meetings, manage deals, and stay in sync with a Node backend. Every request is scoped by a namespaced org and user identifier, sessions live in Postgres, and which capabilities run depends on which integrations are connected: Gmail for delivery, Calendar for scheduling. Google ADK was the constant across the whole timeline. The question that changed over ten months wasn't "ADK or not," it was how much control should live in prompts versus explicit graphs and checkpoints.

The 1.x product

The early architecture was straightforward. FastAPI validated the org and loaded the session, a root agent built with integration-gated sub-agents handled the request, and an ADK Runner executed it. Research started as a SequentialAgent pipeline: pull account data, generate email ideas, generate go-to-market angles.

That shape worked for demos and first integrations. It stopped working once research turned into 12+ logical steps, calendar added proactive scheduling and debrief paths on top of simple booking, and cadences needed a human to approve steps before they fired off emails.

The orchestrator-worker redesign, still on ADK 1.x

The fix, without leaving ADK 1.x, was to stop letting the root agent infer "what's next" from chat history and make the state explicit instead. That became SalesOrchestrator: a root agent managing 40+ worker agents, each built with an LlmAgent factory and exposed to the root as a SafeAgentTool wrapper: a thin layer that recovers from missing arguments or a hallucinated tool name, both of which happen more often than any planner-based design wants to admit.

Underneath the orchestrator sat a session-backed MultiWorkflowState, driven by a set of tools (initialize_workflow, update_workflow_step, switch_workflow, retry_workflow_from_failed_step) that tracked which step a session was on, which of several concurrent workflows it belonged to, and what to do when a step failed. Every worker followed the same internal shape (READ, GROUND, ACT, TEARDOWN), so state stayed predictable even as the worker count grew.

One ADK 1.x constraint shaped this directly: Agent.sub_agents has to be set during __init__. You cannot add sub-agents dynamically once the agent exists, full stop. The workaround was hybrid initialization: build every worker up front, then rebuild the orchestrator and planner state fresh on each request instead of reusing instances. That turned out to matter, since reused instances were a real source of state corruption before that fix landed.

What it cost us

The orchestrator-worker redesign shipped real features (calendar, cadence, deal management with human approval), and it worked. It also had a real, ongoing operational cost:

  • The planner would redundantly replan mid-workflow, even with hard-locked execution modes meant to stop it.
  • Progress tracking duplicated work a graph runtime should have handled for free: every step transition was bookkeeping we wrote and maintained ourselves.
  • Human-in-the-loop was a custom confirm step bolted onto the tool-call loop, not a native interrupt with resume semantics.
  • Every new domain meant more planner instructions and more update_workflow_step choreography in code. The system scaled in workers fine; it scaled in planner complexity badly.

The ADK 2.0 migration

ADK 2.0 replaces hand-built orchestration with Workflow graphs and checkpointed nodes. We ran the migration on a separate branch, and treated it as an architecture swap, not a version bump: google-adk==2.0.0 in place of 1.16.0, with an entirely different entry point.

Research went first, as the vertical slice. The pipeline became a graph: linear steps for prep, execution, and scoring, then a parallel fan-out across org chart, email, GTM, and partnership POV using asyncio.gather, then a join that saves the account plan. When a step fails, ADK's own checkpoint and resume takes over instead of our own FAILED-step bookkeeping.

# sales_agents/workflows/root.py
root_workflow = Workflow(
    name="sales_agent",
    edges=[("START", coordinator)],
)

Domain workflows for cadence, account, deal, delivery, stakeholder, calendar, draft, artifact, enrich, and memory followed, each with its own test suite. Then came the cutover commit, the one that actually decommissioned the custom engine: SalesOrchestrator, SafeAgentTool, and worker registration deleted from the agent module; the 1.x workflow state machine deleted from the tools layer. Combined, that's roughly 1,600 lines of hand-built orchestration removed in a single commit, replaced by a pointer to the new workflow graph.

Routing got simpler at the same time. A single-turn domain_classifier reads the request and returns one domain token. A coordinator node maps that token to the right domain workflow. There's no free-form step planning at the root, and no more hallucinated tool names, because there's no tool-call loop deciding what runs next. rerun_on_resume=True keeps interrupt recovery clean, and an interrupt_id prefix routes a resumed session straight back to the right domain without re-running classification.

Human approval became a first-class interrupt instead of a custom worker: the workflow yields via RequestInput, waits, and resumes on the same session_id when the backend responds, with workflow_pending_actions and a respond API keeping backend and workflow on the same session instead of spawning a disjoint one. Calendar's proactive scheduling flow, where a backend-triggered request has to PATCH a pending action and resume the exact session that's waiting on it, was the sharpest edge case here and the first thing we wrote a manual test script for.

Before and after

ADK 1.x, production baseline:

  • google-adk==1.16.0
  • Entry point: SalesOrchestrator + worker AgentTools
  • Workflow state: MultiWorkflowState plus a hand-written workflow_tools.py
  • HITL: custom confirm steps in the tool loop
  • Failure handling: manual FAILED-step bookkeeping and retry logic

ADK 2.0, migration branch:

  • google-adk==2.0.0
  • Entry point: root_workflowcoordinator → domain workflow
  • Workflow state: Python graphs plus native ADK checkpoints
  • HITL: RequestInput yields, resumed on the same session
  • Failure handling: checkpoint and resume, built into the runtime

What held up, and what didn't

The domain knowledge (research steps, calendar paths, cadence logic) carried over almost untouched. What got deleted was the spine connecting it: the planner, the replanning loop, and the state machine we'd built by hand to make ADK 1.x behave like a workflow engine it was never designed to be.

What survived every single refactor, on both branches, was the multi-tenancy layer: org validation up front, namespaced org and user identifiers, integration-gated capabilities, and external logging. None of that is ADK's job, and none of it should move when your orchestration model changes underneath it.

Lessons for teams building on ADK

  1. ADK 1.x is genuinely good at agent-and-tool composition, and weak at long deterministic pipelines. SequentialAgent covers a simple pipeline fine. Once you have parallel research legs, calendar pathing, and multiple concurrent workflows per session, you either build a state machine yourself or move orchestration into a graph runtime that has one built in.
  2. Putting the planner in the loop scales poorly. Dynamic step planning is flexible right up until it's expensive and fragile. Splitting "LLM for classification" from "code for sequencing" is a better default than it sounds.
  3. Session state is not a workflow database. Tiered memory and a JSON workflow blob in session state can get you most of the way, but checkpointing and resume semantics belong in the runtime, not in application code.
  4. HITL should be a runtime primitive, not a bolted-on confirm step. The difference between a demo and something that survives production is usually whether human approval is a first-class interrupt with a stable session to resume, or a special case someone had to remember to wire up.
  5. Keep the domain workers, swap the spine. A strangler-pattern migration (reuse what the workers already know how to do, delete the orchestrator that was routing to them) is a lot cheaper than a rewrite.
  6. Multi-tenancy is orthogonal to orchestration. If your org scoping, session namespacing, and integration gating are tangled up with your planner, you'll pay for that coupling twice: once now, and again the next time you change the orchestration model.

This is the part of AI engineering that doesn't show up in a demo video: the year of work between a working prototype and a system that checkpoints correctly, resumes on the right session, and doesn't silently drift its own plan mid-conversation. It's also the part we spend most of our time on. If you're staring down a similar rebuild (an agent system that's outgrown its own planner), that's exactly the kind of problem we like to get called in on.

Moyenul Islam

Moyenul Islam

Lead Backend Engineer

Moyenul leads backend engineering at Riverborn, architecting the agent orchestration, session, and infrastructure layers behind our production AI systems. He works across the stack from database schema to runtime orchestration, with a focus on systems that hold up under real production load rather than just demo conditions.

Published by Moyenul Islam

Ready to ship production-grade AI?

Free. 30 minutes. No prep required.