This article provides a step by step architectural and deployment guide for StreamForge, a real-time event-driven multi-agent pipeline built with the Google Agent Development Kit (ADK), open-weights Gemma 3 (gemma3:12b), Google Cloud Eventarc, Cloud Pub/Sub, and Google Cloud Run.
Repository: github.com/RobinaMirbahar/streamforge-event-adk
Enterprise data platforms operate on real-time state changes: a product engineering team uploads a release specification (vertex-vector-sync-v4.2.json) to a Google Cloud Storage bucket, a streaming job commits a partition to BigQuery, or a partner service publishes a schema event to Cloud Pub/Sub. Connecting Google Cloud Eventarc directly to a multi-agent Google ADK workflow runtime on Cloud Run triggers autonomous workflows within 200 ms of an object landing in storage while scaling to 0 instances when the bucket is idle.
Why Combine Eventarc with ADK ParallelAgent and LoopAgent?
Processing a multi-regional product release requires three distinct tasks: extracting technical benchmarks from the raw JSON payload, translating launch messaging across target locales (en_US, de_DE, ja_JP), and simulating 12-month Annual Recurring Revenue (ARR) expansion under regional B2B currency rules. Running those three steps sequentially increases end-to-end latency, while running them in parallel without a verification step risks publishing unrounded foreign-exchange prices.
| Architecture Pattern | Trigger Mechanism | Execution Topology | Cross-Agent Consistency Check | Duplicate Event Protection |
|---|---|---|---|---|
| 🥇 Eventarc + ADK ParallelAgent & LoopAgent | Real-time CloudEvents v1.0 (< 200 ms lag) |
3 concurrent branches (3x fan-out) + LoopAgent critic
|
Iterative audit repairs EUR/JPY pricing until quality_score == 1.0
|
Firestore ce-id ledger short-circuits replays in < 5 ms
|
| 🥈 Scheduled ADK SequentialAgent | Fixed cron schedule | Linear 3-stage chain | Single-pass sequential handoff | Date-keyed Firestore lock |
| 🥉 Single Monolithic Prompt | Manual human chat prompt | Single LLM call | Unchecked single-pass output | Manual retry handling |
End-to-End Event-Driven Architecture on Google Cloud
-
Tier 1: Event Ingestion and Routing: Uploading
releases/2026-10/vertex-vector-sync-v4.2.jsontogs://streamforge-launch-inboxemits a nativegoogle.cloud.storage.object.v1.finalizedevent. Google Cloud Eventarc wraps the notification in a CNCF CloudEvents v1.0 envelope (ce-id,ce-type,ce-source,ce-time) and delivers it over an OIDC-authenticated Cloud Pub/Sub push subscription. -
Tier 2: Serverless Multi-Agent Runtime (Cloud Run): A private FastAPI receiver checks the incoming
ce-idheader (ce-98104271-gcs-finalized) against Cloud Firestore. New events execute the Google ADK workflow (ParallelAgent→LoopAgent→LlmAgent) powered by Gemma 3 12B viaLiteLlm. -
Tier 3: State and Downstream Sink: The final publisher agent validates the output against the Pydantic
EventDrivenLaunchManifestschema and commits both the manifest (MANIFEST-SF-VECTOR-SYNC-4.2) and thece-iddeduplication record to Cloud Firestore.
Orchestrating ParallelAgent Fan-Out and LoopAgent Self-Correction
In Google ADK, all branches inside a ParallelAgent share the same InvocationContext.session.state. Assigning a distinct output_key to each branch isolates their writes cleanly:
-
TechnicalSpecExtractorAgentwrites engineering benchmarks tooutput_key="extracted_specs". -
GlobalLocalizationAgentwrites localized headlines and draft currency tiers tooutput_key="localized_copy". -
RevenuePricingSimulatorAgentwrites the 12-month ARR forecast and regional FX compliance rules tooutput_key="pricing_simulation".
Once all 3 parallel branches complete, QualityCritiqueRefinementLoop (LoopAgent with max_iterations=3) invokes LaunchReadinessCriticAgent to audit the combined state. When GlobalLocalizationAgent converts the $499/mo Growth Tier at the raw 0.92 EUR exchange rate (€459.08/mo), Iteration 1 of the LoopAgent flags the violation against the de_DE rule requiring VAT-exclusive B2B prices rounded to the nearest €5 (quality_score = 0.72) and updates the pricing tier to €460/mo. On Iteration 2, the audit reaches quality_score = 1.0 and invokes exit_quality_loop(), setting tool_context.actions.escalate = True to exit the loop immediately.
At This Point You Should Have
- Python
3.10+(3.11recommended) withgoogle-adk>=1.0.0,litellm,fastapi, andpydanticinstalled. - A Google Cloud project with
Cloud Run,Eventarc,Cloud Pub/Sub,Cloud Storage, andCloud FirestoreAPIs enabled. - Access to Gemma 3 12B (
ollama_chat/gemma3:12blocally via Ollama orgoogle/gemma-3-12b-iton a Cloud RunNVIDIA L424 GBGPU service).
Step 1: Define the CloudEvent and Launch Manifest Schemas
The pipeline enforces typed contracts at both boundaries: CloudEventEnvelope validates the incoming Eventarc HTTP headers and payload, while EventDrivenLaunchManifest constrains the final ReleasePublisherAgent output in streamforge_agent/schemas.py.
==> [1/6] Enabling Cloud Run, Eventarc, Pub/Sub, Storage, and Firestore APIs...
==> [2/6] Creating Cloud Storage Inbox Bucket & Dead-Letter Pub/Sub Topic...
==> [3/6] Creating Least-Privilege Service Accounts...
==> [4/6] Deploying StreamForge ADK Service to Cloud Run (private ingress)...
Service [streamforge-event-adk] revision [streamforge-event-adk-00001-x8k] has been deployed and is serving 100 percent of traffic.
==> [5/6] Granting Cloud Run Invoker + Eventarc EventReceiver to Eventarc SA...
==> [6/6] Creating Eventarc Trigger for google.cloud.storage.object.v1.finalized...
Created trigger [streamforge-gcs-release-trigger] in [us-central1].
Step 2: Implement the Parallel Enrichment and Loop Escalation Tools
In streamforge_agent/tools.py, each specialist tool exposes typed parameters and docstrings for Gemma 3 function calling, while exit_quality_loop sets tool_context.actions.escalate = True on the injected ADK ToolContext.
==> [1/6] Enabling Cloud Run, Eventarc, Pub/Sub, Storage, and Firestore APIs...
==> [2/6] Creating Cloud Storage Inbox Bucket & Dead-Letter Pub/Sub Topic...
==> [3/6] Creating Least-Privilege Service Accounts...
==> [4/6] Deploying StreamForge ADK Service to Cloud Run (private ingress)...
Service [streamforge-event-adk] revision [streamforge-event-adk-00001-x8k] has been deployed and is serving 100 percent of traffic.
==> [5/6] Granting Cloud Run Invoker + Eventarc EventReceiver to Eventarc SA...
==> [6/6] Creating Eventarc Trigger for google.cloud.storage.object.v1.finalized...
Created trigger [streamforge-gcs-release-trigger] in [us-central1].
Step 3: Wire ParallelAgent, LoopAgent, and SequentialAgent
In streamforge_agent/agent.py, build_streamforge_event_pipeline() composes the 3 parallel enrichment agents, the iterative quality critic loop, and the schema-constrained publisher agent into a single parent SequentialAgent.
==> [1/6] Enabling Cloud Run, Eventarc, Pub/Sub, Storage, and Firestore APIs...
==> [2/6] Creating Cloud Storage Inbox Bucket & Dead-Letter Pub/Sub Topic...
==> [3/6] Creating Least-Privilege Service Accounts...
==> [4/6] Deploying StreamForge ADK Service to Cloud Run (private ingress)...
Service [streamforge-event-adk] revision [streamforge-event-adk-00001-x8k] has been deployed and is serving 100 percent of traffic.
==> [5/6] Granting Cloud Run Invoker + Eventarc EventReceiver to Eventarc SA...
==> [6/6] Creating Eventarc Trigger for google.cloud.storage.object.v1.finalized...
Created trigger [streamforge-gcs-release-trigger] in [us-central1].
Step 4: Verify the Pipeline and CloudEvent Deduplication Locally
Run scripts/simulate_cloudevent_pipeline.py to execute the 3-branch ParallelAgent fan-out, inspect the 2-iteration LoopAgent self-correction trace, and verify the ce-id idempotency guard against a duplicate Pub/Sub delivery.
==> [1/6] Enabling Cloud Run, Eventarc, Pub/Sub, Storage, and Firestore APIs...
==> [2/6] Creating Cloud Storage Inbox Bucket & Dead-Letter Pub/Sub Topic...
==> [3/6] Creating Least-Privilege Service Accounts...
==> [4/6] Deploying StreamForge ADK Service to Cloud Run (private ingress)...
Service [streamforge-event-adk] revision [streamforge-event-adk-00001-x8k] has been deployed and is serving 100 percent of traffic.
==> [5/6] Granting Cloud Run Invoker + Eventarc EventReceiver to Eventarc SA...
==> [6/6] Creating Eventarc Trigger for google.cloud.storage.object.v1.finalized...
Created trigger [streamforge-gcs-release-trigger] in [us-central1].
==> [1/6] Enabling Cloud Run, Eventarc, Pub/Sub, Storage, and Firestore APIs...
==> [2/6] Creating Cloud Storage Inbox Bucket & Dead-Letter Pub/Sub Topic...
==> [3/6] Creating Least-Privilege Service Accounts...
==> [4/6] Deploying StreamForge ADK Service to Cloud Run (private ingress)...
Service [streamforge-event-adk] revision [streamforge-event-adk-00001-x8k] has been deployed and is serving 100 percent of traffic.
==> [5/6] Granting Cloud Run Invoker + Eventarc EventReceiver to Eventarc SA...
==> [6/6] Creating Eventarc Trigger for google.cloud.storage.object.v1.finalized...
Created trigger [streamforge-gcs-release-trigger] in [us-central1].
Step 5: Deploy the Private Cloud Run Receiver and Eventarc Trigger
Run deploy/deploy_eventarc_adk.sh to deploy the container to Google Cloud Run (2 vCPU, 2 GiB memory, --no-allow-unauthenticated, --min-instances=0, --max-instances=10) and bind the Eventarc trigger to google.cloud.storage.object.v1.finalized.
==> [1/6] Enabling Cloud Run, Eventarc, Pub/Sub, Storage, and Firestore APIs...
==> [2/6] Creating Cloud Storage Inbox Bucket & Dead-Letter Pub/Sub Topic...
==> [3/6] Creating Least-Privilege Service Accounts...
==> [4/6] Deploying StreamForge ADK Service to Cloud Run (private ingress)...
Service [streamforge-event-adk] revision [streamforge-event-adk-00001-x8k] has been deployed and is serving 100 percent of traffic.
==> [5/6] Granting Cloud Run Invoker + Eventarc EventReceiver to Eventarc SA...
==> [6/6] Creating Eventarc Trigger for google.cloud.storage.object.v1.finalized...
Created trigger [streamforge-gcs-release-trigger] in [us-central1].
==> [1/6] Enabling Cloud Run, Eventarc, Pub/Sub, Storage, and Firestore APIs...
==> [2/6] Creating Cloud Storage Inbox Bucket & Dead-Letter Pub/Sub Topic...
==> [3/6] Creating Least-Privilege Service Accounts...
==> [4/6] Deploying StreamForge ADK Service to Cloud Run (private ingress)...
Service [streamforge-event-adk] revision [streamforge-event-adk-00001-x8k] has been deployed and is serving 100 percent of traffic.
==> [5/6] Granting Cloud Run Invoker + Eventarc EventReceiver to Eventarc SA...
==> [6/6] Creating Eventarc Trigger for google.cloud.storage.object.v1.finalized...
Created trigger [streamforge-gcs-release-trigger] in [us-central1].
Official Google Cloud and ADK Documentation
- Google Agent Development Kit (ADK) Documentation
- Google ADK Workflow Agents (
SequentialAgent,ParallelAgent,LoopAgent) - Google ADK Python SDK Repository (
google/adk-python) - Google Gemma Open Models Documentation
- Google Cloud Eventarc Documentation
- Google Cloud Run Documentation
- Google Cloud Pub/Sub Push Subscriptions Guide
- Google Cloud Firestore Documentation
Summary
The goal of this article was to build and verify a real-time, event-driven multi-agent pipeline on Google Cloud using Eventarc, Cloud Pub/Sub, Cloud Run, open-weights Gemma 3, and the Google Agent Development Kit. The key to the solution was pairing ParallelAgent for concurrent 3-branch fan-out with LoopAgent for iterative cross-branch self-correction and a Cloud Firestore ce-id deduplication ledger. The verification results were:
- 🟢 Concurrent Fan-Out (
ParallelAgent): Extracted engineering specs (73.8%latency drop from145 msto38 msat250,000rows/sec), localized copy across3locales (en_US,de_DE,ja_JP), and forecasted$2,856,000in 12-month Net New ARR lift across340upgraded accounts (18.5%of1,840active Growth Tier accounts at$8,400annual expansion). - 🟢 Self-Correcting Quality Loop (
LoopAgent): Detected the€459.08/moand¥75,848/mocurrency formatting violations on Iteration1(quality_score = 0.72), repaired them to€460/moand¥75,800/mo, and exited cleanly on Iteration2(quality_score = 1.0) viaexit_quality_loop(). - 🟢 At-Least-Once Idempotency: Short-circuited duplicate
ce-98104271-gcs-finalizedredelivery in< 5 mswith0redundant parallel branches or LLM calls.
The strategy for orchestrating event-driven multi-agent pipelines with Google ADK and Gemma 3 on Cloud Run was validated with an incremental step by step approach.


Top comments (2)
For teaching, I'd want to review the four-panel script before generating the images. If the RAG explanation is wrong, I'd rather fix it while it's still text than after I've got four lovely panels explaining the wrong thing. Could that be an optional pause in the workflow?
Thanks so much for reading closely, @stefan_07d23f966225156b50 ! Have you tried building something similar for your own teaching materials yet? Burning image generation credits on four panels only to realize Panel 2 explained vector retrieval backwards is painful, both for teaching accuracy and your cloud bill.
Yes, you can add an optional human review pause right between the script stage and the image rendering stage. Because Cloud Run bills by active request time, you don't want the container sitting idle waiting for a human click. The cleanest way to do this in Google ADK + Eventarc is a two-stage state handoff using Firestore (or ADK session state):
Stage 1 (Script Draft Only): Run the storyboard agent to generate the 4-panel text and save it to Firestore (or
session.state["panel_script"]) withstatus: "PENDING_REVIEW". If you pass a flag likeauto_render: false, the pipeline stops right here and returns the editable JSON text for all 4 panels.Human Edit & Approval: You review the RAG explanation in text, tweak the dialogue or visual prompts on any panel you don't like, and flip
statusto"APPROVED".Stage 2 (Render on Approval): An Eventarc trigger listening for
google.cloud.firestore.document.v1.updated(wherestatus == "APPROVED", or a simple/render-approvedendpoint call) wakes up the second ADK agent, pulls your editedpanel_script, and fans out the 4 image generation calls.If you are running it inside a single ADK runner with
session_service, it looks like this:Bonus tip if you build this for teaching:
When you pause at the text stage, have the script agent also output a shared
character_biblefield in the JSON (for example,"Robot tutor wearing round brass glasses and a navy hoodie, flat vector style") and prepend that exact string to all 4 panel prompts during Stage 2. Also, add adirty_panels: [2]parameter to your render endpoint so if Panel 2's visual metaphor still looks slightly off after rendering, you can re-roll just Panel 2 without regenerating Panels 1, 3, and 4!