[AI in Action] Building a LINE AI Business Card Master with Go and Google ADK 2.0: From Fan-Out Parallel Retrieval to HITL Pause
Recap During the AI Agent development craze over the past year, the first Agent many developers wrote often looked like this: writing an incredibly long System Instruction, then stuffing five or six Tools into the same
Recap
During the AI Agent development craze over the past year, the first Agent many developers wrote often looked like this: writing an incredibly long System Instruction, then stuffing five or six Tools into the same LLMAgent, and praying the model would obediently follow steps A β B β C β D.
It looks impressive as a Demo initially, but as soon as it hits a production environment, problems immediately surface:
- The more steps, the higher the rate of losing control: When a Prompt is stuffed with "First do OCR, then check the database for duplicates, simultaneously search online for what this company does, then assemble a preview card to ask the user, if the user wants to change the job title then regenerate the preview, and only write to the DB and write an icebreaker email after confirmation," the model occasionally skips the duplicate check or writes directly to the database before waiting for user confirmation.
- Burning money and waiting in places where Tokens shouldn't be spent: Logic that is 100% deterministic, such as "shunting based on LINE event type" or "normalizing phone number formats," is not only slow when handed to an LLM but also prone to errors.
- Independent tasks are forced to execute in a queue: Checking the database, Google searching company backgrounds, and comparing recent conversation records are clearly independent of each other, yet they become sequential waits due to the linear conversation loop of a single Agent.
To solve this pain point, Google officially introduced a heavyweight feature in Agent Development Kit (ADK) 2.0 ββ Graph Workflows, with simultaneous support for Python and Go (google.golang.org/adk/v2/workflow)!
After seeing this update, I immediately decided to use my most familiar Golang + LINE Messaging API SDK v8 to build a complete open-source project line-card-assistant (LINE AI Business Card Master). I put all the core weapons of ADK 2.0 Graph Workflow (AddRoute conditional routing, AddFanOut / AddFanIn parallel processing and aggregation, NewRequestInputEvent Human-in-the-Loop pause/wait and loop correction) through a real-world battle test and deployed it on Google Cloud Run.
This article will first guide you through two core technical documents that are must-reads for Google's official ADK Graph Workflows, and then break down step-by-step how we used Go code to implement these graph workflow patterns in a LINE Bot!
I. How Google Officially Defines Graph Workflow? Analysis of Two Must-Read Guides
Before looking at the code, it is strongly recommended to understand the core philosophy of the ADK team's design for Graph Workflows. Google recently released two very critical developer guides:
- Google Cloud Official Blog: Graph Workflows in ADK: Everything You Need to Know (Written by Annie Wang and Shangjie Chen of the ADK team)
- ADK Official Technical Documentation: Graph-based agent workflows (adk.dev/graphs)
We have condensed and organized the essence of these two official articles as follows:
1. What is "Graph Engineering"?
In the article "Graph Workflows in ADK: Everything You Need to Know", the authors use a "Customer Refund Workflow" as an example to point out a core concept:
As workflows become more complex, reliability comes from separating "Workflow Control" from "Model Reasoning." The essence of Graph Engineering is to decompose tasks into independent nodes, connect them with edges, and explicitly decide at each step whether "Code," "Models," or "People" should determine the next step.
The article summarizes five of the most commonly used Graph Workflow design patterns in the real world:
| Core Pattern | What pain point does it solve? | ADK 2.0 Corresponding Mechanism |
|---|---|---|
| 1. Fan-out and Fan-in Parallel Aggregation | Multiple independent data collection steps (e.g., checking orders, payments, customer records) slow down overall latency if executed sequentially. | Use AddFanOut to trigger multiple nodes to execute in parallel, then use a JoinNode (AddFanIn) to wait for all branches to complete before aggregating, consuming zero extra LLM calls in between. |
| 2. Deterministic Routing vs. Agent Routing | Some shunting rules are black and white (e.g., amount > $100 goes to manual review) and shouldn't be guessed by an LLM; others require understanding natural language intent. |
Deterministic Router: Pure code logic sets ev.Routes, 0 Token consumption, 100% testable. Agent Router: Intent is judged by the LLM, but graph edges strictly limit valid jump targets. |
| 3. Human-in-the-Loop (HITL) Pause and Resume | High-risk actions (e.g., large refunds, writing to production databases) must be confirmed by a human, but traditional Webhooks are stateless short connections. | A node issues a RequestInput event to freeze the execution state (Checkpoint) and pause the Graph; after receiving an external human response, RerunOnResume wakes up the same node to continue. |
| 4. Parallel Workers vs. Fan-out | Distinguishes between "performing three different checks on the same data" and "performing the same processing on N items." |
Fan-out: Different nodes perform different responsibilities. Parallel Workers: The same node expands in parallel for array elements. |
| 5. Static Graph vs. Dynamic Orchestration | How to balance "architectural visualization/maintainability" with "dynamic loop correction"? | Use static Edges to define clear routing and Conditional Cycles (e.g., generate draft β check/modify β regenerate), making the entire system's state machine clear at a glance. |
2. The Four Core Building Blocks of ADK Go v2.0 (adk.dev/graphs)
In the official document "Graph-based agent workflows", the API design for ADK 2.0 in the Go language (google.golang.org/adk/v2/workflow) is shown in detail. To assemble a Graph in Go, you mainly need to master the following four components:
-
Nodes:
-
workflow.NewFunctionNode/workflow.NewEmittingFunctionNode: Wraps any standard Go function into a graph node, suitable for DB queries, data cleaning, API calls, or assembling LINE Flex Messages. -
llmagent.RunLLMAgentAsNode: Encapsulates anllmagentwith a specific Prompt and tools (e.g.,geminitool.GoogleSearch{}) into a single node, allowing the LLM to focus on doing "one thing" well.
-
-
State & Data Flow:
- In addition to passing strongly-typed
Outputbetween nodes, structured data can be written to the Session State (ctx.Session().State()) viaev.Actions.StateDeltafor downstream or aggregation nodes to read.
- In addition to passing strongly-typed
-
Edges & EdgeBuilder:
-
eb.Add(from, to): Unidirectional straight connection. -
eb.AddRoute(from, workflow.StringRoute("route_name", targetNode), ...): Conditional branch routing (can also be used to create Cycles!). -
eb.AddFanOut(from, branch1, branch2, ...)andeb.AddFanIn(joinNode, branch1, branch2, ...): Parallel branching and synchronous aggregation.
-
-
WorkflowAgent:
- Finally, call
workflowagent.New(workflowagent.Config{Edges: edges}), and the entire Graph becomes a standard ADKagent.Agent, which is then handed over to therunner.Runnerfor execution and Session management!
- Finally, call
II. Practical Project Architecture: LINE AI Business Card Master (line-card-assistant)
Having understood the official theory, let's see how to apply these concepts to a real-world LINE Bot scenario.
Business professionals receive a large number of paper business cards at exhibitions or meetings every day. Traditional business card apps only offer simple OCR recognition but lack features like "What has this company been doing lately?", "At which event did I meet this person?", and "How can I correct recognition errors directly by speaking in the chat room?".
Therefore, we designed line-card-assistant, with an overall architecture that fully corresponds to ADK 2.0's Graph Workflow:
[LINE Webhook]
β
βΌ
[Router Node (AddRoutes)]
βββ (Received business card photo) βββΊ [Vision OCR Node]
β β
β (AddFanOut Parallel Processing)
β ββββββββββββββββΌβββββββββββββββ
β βΌ βΌ βΌ
β [DB Dup Check & Validation] [Company Background Search] [Past Interaction Retrieval]
β ββββββββββββββββΌβββββββββββββββ
β (AddFanIn Aggregation)
β βΌ
β [Generate Flex Message Preview Card]
β β
β βΈοΈ NewRequestInputEvent (HITL Pause & Wait)
β β
β ββββββββββββββββββββββ΄βββββββββββββββββββββ
β βΌ (Click: Confirm Save) βΌ (Click: Modify Field / Add Note)
β [Write to DB + Generate Icebreaker Draft] [Re-entry Loop Correction Node]
β
βββ (Received query text) βββΊ [Query Rewrite] βββΊ [SQL + Vector Hybrid Retrieval] βββΊ [Flex Carousel Card Rotation]
This architecture diagram covers three core paths:
- Business Card Recognition and Enrichment Pipeline (Ingest Pipeline): Receive photo β Vision OCR β Execute three tasks in parallel (DB duplicate check + Google Search company background + Conversation context inference for meeting occasion) β Aggregate to generate LINE Flex preview card β HITL pause to wait for user confirmation or modification.
-
HITL Loop Correction (Re-entry Loop): When the graph is paused at
HITLConfirmNode, if the user enters text directly in LINE (e.g., "Change job title to Director, met at Cloud Summit"), the workflow wakes up and takes theRouteEditbranch intoApplyEditNode, updates the fields, and loops back toBuildPreviewNodeto regenerate the preview card and pause again, until the user clicks "β Confirm Save" to actually write to the database and generate the icebreaker email! - Natural Language Hybrid Retrieval Pipeline (Search Pipeline): Receive query text (e.g., "Help me find people doing AI I met at Cloud Summit last week") β Query Rewrite structural decomposition β SQL + Keyword/Tag hybrid retrieval β Return LINE Flex Carousel business card rotation.
III. Go Code Deep Dive: Writing Graph Workflow into a LINE Bot
Next, let's look directly at the Go implementation of internal/workflow/graph.go and various nodes to see how clean the ADK 2.0 API looks in Go.
1. A Global View: Assembling the Graph with EdgeBuilder
In internal/workflow/graph.go, we declare and connect all nodes via the BuildEdges function. You can clearly see the topological structure of the entire graph:
func BuildEdges(deps *Dependencies) ([]workflow.Edge, error) {
// 1. Entry Router Node
routerNode, _ := NewRouterNode()
// 2. Business Card Recognition and Fan-Out Three Parallel Nodes
visionOCRNode, _ := NewVisionOCRNode(deps)
cleanAndDedupNode, _ := NewCleanAndDedupNode(deps)
companyEnrichNode, _ := NewCompanyEnrichNode(deps)
contextMatchNode, _ := NewContextMatchNode(deps)
joinPreviewNode := workflow.NewJoinNode("JoinPreviewNode")
// 3. Preview Card Generation, HITL Pause, and Confirm/Modify Branch Nodes
buildPreviewNode, _ := NewBuildPreviewNode(deps)
hitlConfirmNode, _ := NewHITLConfirmNode(deps)
saveCardNode, _ := NewSaveCardNode(deps)
applyEditNode, _ := NewApplyEditNode(deps)
// 4. Natural Language Search and Icebreaker Email Nodes
queryIntentNode, _ := NewQueryIntentNode(deps)
hybridSearchNode, _ := NewHybridSearchNode(deps)
broadSearchNode, _ := NewBroadSearchNode(deps)
formatCarouselNode, _ := NewFormatCarouselNode(deps)
emailDraftNode, _ := NewEmailDraftNode(deps)
eb := workflow.NewEdgeBuilder()
// Entry -> Deterministic Router
eb.Add(workflow.Start, routerNode)
eb.AddRoute(routerNode,
workflow.StringRoute(RouteImage, visionOCRNode),
workflow.StringRoute(RouteText, queryIntentNode),
workflow.StringRoute(RoutePostbackDraft, emailDraftNode),
)
// Ingest Pipeline: Vision OCR -> Parallel Fan-Out -> JoinNode Fan-In -> Preview
eb.AddFanOut(visionOCRNode, cleanAndDedupNode, companyEnrichNode, contextMatchNode)
eb.AddFanIn(joinPreviewNode, cleanAndDedupNode, companyEnrichNode, contextMatchNode)
eb.Add(joinPreviewNode, buildPreviewNode)
eb.Add(buildPreviewNode, hitlConfirmNode)
// Conditional branches after HITL pause: Confirm Save vs. Modify Field Loop
eb.AddRoute(hitlConfirmNode,
workflow.StringRoute(RouteConfirm, saveCardNode),
workflow.StringRoute(RouteEdit, applyEditNode),
)
// β
Key Cycle: After modifying fields, route back to buildPreviewNode to regenerate Flex preview card and enter HITL again!
eb.AddRoute(applyEditNode,
workflow.StringRoute(RoutePreview, buildPreviewNode),
)
// Search Pipeline: Query Rewrite -> Hybrid Search -> (Found / Empty Fallback) -> Carousel
eb.Add(queryIntentNode, hybridSearchNode)
eb.AddRoute(hybridSearchNode,
workflow.StringRoute(RouteFound, formatCarouselNode),
workflow.StringRoute(RouteEmpty, broadSearchNode),
)
eb.Add(broadSearchNode, formatCarouselNode)
return eb.Build()
}
This code perfectly embodies the Static Graph advantage mentioned in the Google Cloud Blog: you don't need to flip through thousands of lines of Prompts or track down Tool Calls scattered everywhere. Just by looking at the 30 lines of code in BuildEdges, you can 100% grasp all possible paths and loop boundaries of the entire Agent system.
2. Deterministic Router: Precise Shunting with 0 Token Consumption
When the LINE Webhook receives an event, we don't need to waste a Gemini API call to ask the model "Is the user sending a picture or text?". We use workflow.NewEmittingFunctionNode to create a pure code deterministic routing node RouterNode:
const (
RouteImage = "route_image"
RouteText = "route_text"
RoutePostbackDraft = "route_postback_draft"
)
func NewRouterNode() (workflow.Node, error) {
return workflow.NewEmittingFunctionNode("RouterNode", func(ctx workflow.Context, _ struct{}) (string, error) {
inputType, err := getStateString(ctx, StateKeyInputType)
if err != nil || inputType == "" {
inputType = "text"
}
var route string
switch inputType {
case "image":
route = RouteImage
case "postback_draft":
route = RoutePostbackDraft
default:
route = RouteText
}
// Emit an event with Routes tags to let EdgeBuilder's AddRoute decide the next node
ev := workflow.NewEvent(ctx, "route_selected")
ev.Routes = []string{route}
if err := ctx.Emit(ev); err != nil {
return "", err
}
return route, nil
}, workflow.WithDeclaredRoutes(RouteImage, RouteText, RoutePostbackDraft))
}
As long as you set ev.Routes = []string{route} when emitting a workflow.Event, and use workflow.WithDeclaredRoutes(...) to validate route legitimacy during construction, ADK will automatically check for misspelled Route names during the startup phase!
3. Fan-Out / Fan-In Parallel Processing: Running DB Dup Check, Google Search, and Context Inference Simultaneously
Once VisionOCRNode extracts a structured models.BusinessCard from the business card photo via gemini-2.5-flash's multimodal capabilities, we have three completely independent tasks to perform:
-
CleanAndDedupNode(Pure Code + DB Query): Normalizes Taiwan mobile and landline numbers to+886-...format and queries the SQLite database to see if a business card for the same person already exists (if the job title is different, it can even mark a "Promotion/Transfer Reminder"!). -
CompanyEnrichNode(Embeddedllmagentwith Google Search tool): Automatically searches the web for the latest background of the company based on the company name on the business card, condensing it into a 2-sentence summary and 3~5 industry tags. -
ContextMatchNode(LLM Context Inference): Reads the user's most recent LINE conversation records before and after sending the photo (e.g., the user just sent "I'm attending Cloud Summit today") to automatically infer the meeting occasion for this business card (MetContext).
In internal/workflow/nodes_ingest.go, we can even use llmagent.RunLLMAgentAsNode inside CompanyEnrichNode to call a sub-agent with geminitool.GoogleSearch{}:
// Combine LLMAgent with Google Search Grounding capability within a Graph node
searchAgent, err := llmagent.New(llmagent.Config{
Name: "CompanySearchSubAgent",
Description: "Searches the web for company background and industry tags.",
Model: deps.SearchModel,
Instruction: "Given a company name, search Google and return JSON with summary (2 sentences in Traditional Chinese) and tags (3 to 5 industry tags).",
Tools: []tool.Tool{geminitool.GoogleSearch{}},
})
if err == nil {
_ = llmagent.RunLLMAgentAsNode(searchAgent, llmagent.NodeConfig{Name: "CompanySearchSubAgentNode"})
}
π‘ Practical Details of Go Parallel Nodes: How to Avoid Race Conditions?
When running eb.AddFanOut in Go, the three branch nodes execute in parallel in different Goroutines. If three nodes simultaneously read and write to the same BusinessCard JSON object and write back to the same State Key, a Lost Update (last write wins) race condition will occur!
To solve this problem, we adopted a very clean technique in the implementation:
-
Write to independent StateDelta Keys:
-
CleanAndDedupNodeonly writes toStateKeyDedupCard,StateKeyIsDuplicate,StateKeyOldTitle. -
CompanyEnrichNodeonly writes toStateKeyCompanySummaryandStateKeyCompanyTags. -
ContextMatchNodeonly writes toStateKeyMetContext.
-
- ADK 2.0's
Runnerwill sequentially Commit theev.Actions.StateDeltaemitted by each node in the main Goroutine. - Once
JoinPreviewNode(eb.AddFanIn) confirms that the three parallel branches are complete, the single-threadedBuildPreviewNodeusesloadMergedCardFromState(ctx)to safely merge the results of the three branches into a completeBusinessCardand renders a beautiful LINE Flex Message preview card!
4. Human-in-the-Loop (HITL): NewRequestInputEvent Pause and Re-entry Loop Correction
The most exciting part of this entire system is the Human-in-the-Loop pause and resume mechanism.
In traditional LINE Bot development, if you want to achieve "send preview card β wait for user to click confirm or type modification β send new preview card after modification β save after confirmation," you usually have to maintain a complex user_state_machine table in the database yourself.
But in ADK 2.0 Graph Workflow, this is built directly into HITLConfirmNode! Let's see how NewHITLConfirmNode in internal/workflow/nodes_ingest.go is written:
func NewHITLConfirmNode(_ *Dependencies) (workflow.Node, error) {
rerun := true
return workflow.NewEmittingFunctionNode("HITLConfirmNode", func(ctx workflow.Context, _ struct{}) (string, error) {
activeID, _ := getStateString(ctx, StateKeyActiveInterruptID)
// [Phase 1: First entry to node] Not yet paused, emit NewRequestInputEvent to pause Graph!
if activeID == "" {
interruptID := fmt.Sprintf("hitl-card-%d", time.Now().UnixNano())
if err := emitStateDelta(ctx, map[string]any{
StateKeyActiveInterruptID: interruptID,
}); err != nil {
return "", err
}
reqEv := workflow.NewRequestInputEvent(ctx, session.RequestInput{
InterruptID: interruptID,
Payload: map[string]any{
"prompt": "Please confirm if the business card info is correct. Click 'Confirm Save' or enter modifications directly (e.g., change title to Director).",
},
})
if err := ctx.Emit(reqEv); err != nil {
return "", err
}
return "waiting_for_user", nil
}
// [Phase 2: Wake up and rerun after receiving user response (RerunOnResume: true)]
resumedVal, err := ctx.ResumedInput(activeID)
var decision HITLResumePayload
if err == nil && resumedVal != nil {
_ = decodeViaJSON(resumedVal, &decision)
}
// Clear the consumed InterruptID so that if the Edit loop comes back later, it can trigger a pause again!
_ = emitStateDelta(ctx, map[string]any{
StateKeyActiveInterruptID: "",
StateKeyHITLAction: decision.Action,
StateKeyHITLEditText: decision.EditText,
})
route := RouteConfirm
if strings.EqualFold(decision.Action, "edit") || (decision.Action != "confirm" && strings.TrimSpace(decision.EditText) != "") {
route = RouteEdit
}
ev := workflow.NewEvent(ctx, "hitl_decision")
ev.Routes = []string{route}
if err := ctx.Emit(ev); err != nil {
return "", err
}
return route, nil
}, workflow.WithNodeConfig(workflow.NodeConfig{
RerunOnResume: &rerun, // β
Key setting: Rerun this node function when external input resumes
}), workflow.WithDeclaredRoutes(RouteConfirm, RouteEdit))
}
How does the LINE Webhook feed user button clicks or text back to the paused Graph?
When HITLConfirmNode emits a NewRequestInputEvent, our Engine (located in internal/workflow/engine.go) captures this InterruptID and pushes the LINE Flex Message preview card generated by BuildPreviewNode to the user.
Then:
- If the user clicks the "β
Confirm Save" button (Postback Event) on the Flex Message, the LINE Webhook calls
Engine.ResumeHITL(ctx, userID, HITLResumePayload{Action: "confirm"}). - If the user types directly in the chat room "Change title to Technical Director, met at AI conference" (Text Event), the LINE Webhook detects that the user has a
PendingHITLwaiting, automatically intercepts this text, and callsEngine.ResumeHITL(ctx, userID, HITLResumePayload{Action: "edit", EditText: text})!
Inside Engine.ResumeHITL, you only need to send a FunctionResponse with workflow.WorkflowInputFunctionCallName to the ADK Runner, and the frozen Graph will seamlessly continue execution:
msg := &genai.Content{
Role: string(genai.RoleUser),
Parts: []*genai.Part{
{
FunctionResponse: &genai.FunctionResponse{
ID: pending.InterruptID,
Name: workflow.WorkflowInputFunctionCallName,
Response: map[string]any{
"action": payload.Action,
"edit_text": payload.EditText,
},
},
},
},
}
Once it enters RouteEdit, ApplyEditNode corrects the business card data, then loops back to BuildPreviewNode via eb.AddRoute(applyEditNode, workflow.StringRoute(RoutePreview, buildPreviewNode)), generating an updated Flex preview card and stopping again at HITLConfirmNode to wait for user confirmation. This is where Graph Workflow is most powerful for handling multi-turn interactive corrections!
5. Natural Language Hybrid Retrieval Pipeline (Query Rewrite + Hybrid Search + Flex Carousel)
When the user is not waiting for business card confirmation but directly enters query text (e.g., "Help me find people doing AI I met at Cloud Summit last week"), the RouterNode directs the request to the RouteText branch (defined in internal/workflow/nodes_search.go):
-
QueryIntentNode: Uses Gemini to decompose natural language into a structuredSearchFilter(includingKeywords,Tags,Company,Title,MetContext). -
HybridSearchNode: Combines SQL condition filtering with tag/keyword matching and simultaneously acts as a Deterministic Router:- If business cards are found β Emit
RouteFoundto go directly toFormatCarouselNode. - If precise conditions yield no results β Emit
RouteEmptyto automatically enterBroadSearchNodefor fuzzy/relaxed retrieval (Fallback), and finallyFormatCarouselNoderenders up to 10 LINE Flex Carousel business cards, each with a one-click "βοΈ Generate Icebreaker" Postback button!
- If business cards are found β Emit
IV. Deployment: Cloud Run + Self-Identity Security Authentication
After completing development and passing all unit tests (go test ./...), we deployed the service to Google Cloud Run (asia-east1).
In the cloud deployment architecture, we can use Cloud Run's Service Account Self-Identity (Application Default Credentials, ADC) mechanism:
- By binding a Service Account with
roles/aiplatform.userpermissions to Cloud Run and setting environment variablesGOOGLE_GENAI_USE_VERTEXAI=trueandGOOGLE_CLOUD_PROJECT, the application can directly call Vertex AI / Gemini models using the Identity provided by the Cloud Run execution environment, eliminating the risk of managing static API Key leaks. - If developing locally or using the Gemini Developer API, you can easily switch via environment variables:
gcloud run deploy line-card-assistant \
--source . \
--region asia-east1 \
--allow-unauthenticated \
--set-env-vars="LINE_CHANNEL_SECRET=your_secret,LINE_CHANNEL_ACCESS_TOKEN=your_token,GEMINI_MODEL=gemini-2.5-flash"
V. Summary of Pitfalls and Practical Experience
After deeply integrating Google ADK 2.0 Graph Workflow (google.golang.org/adk/v2/workflow) with LINE Bot, there are several practical insights worth sharing with Go developers:
-
Keep Prompts Small, Leave the Flow to the Graph: Previously, a single super Agent required hundreds of lines of Prompts and often missed steps; now, the Prompt for each node (such as
VisionOCRNode,CompanyEnrichNode,ContextMatchNode) is only a few lines long, with a single responsibility and fixed output format, resulting in a quantum leap in stability. -
AddFanOutSignificantly Reduces Perceived Latency: In a LINE chat room, users hate waiting after sending a business card photo. By usingAddFanOutto simultaneously perform DB duplicate checks, Google Search company background investigations, and context inference, the total time taken depends only on the slowest branch, rather than the sum of all three steps. -
Use
StateDeltato Isolate Parallel Writes: When usingAddFanOutin Go, remember to have each parallel branch write to its own dedicated Session State Key, and then merge them uniformly after passing through theJoinNode(AddFanIn) to elegantly avoid concurrent overwrite issues. -
NewRequestInputEvent+RerunOnResumeis a Secret Weapon for Chatbots: Whether it's a LINE Postback button or free text input, they can all be uniformly encapsulated into aFunctionResponseforWorkflowInputFunctionCallNameto wake up the same Graph Session, completely bidding farewell to hand-coded spaghetti-style state machines!
The complete project source code, architecture diagrams, and deployment scripts are all open-sourced on GitHub. You are welcome to clone it and play around, or directly transform it into your own enterprise-grade LINE Agent workflow!
References and Further Reading
- Open Source Code for this Project: kkdai/line-card-assistant (GitHub)
- Google Cloud Official Blog: Graph Workflows in ADK: Everything You Need to Know
- ADK Official Documentation: Graph-based agent workflows - Agent Development Kit (ADK)
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.