GGoldwater.dev
All labs
Hands-on LabIntermediate45–60 min·Updated June 2026

Building with Google's Interactions API

Build a stateful, agentic workflow on Google's Interactions API — multi-turn server-side state, function calling, tool combinations, and background tasks — in one small Python program.

Download starter repo

What you'll build

By the end of this lab you'll have a small but complete Python program that talks to Google's Gemini models through the Interactions API — the interface Google made generally available on June 22, 2026 and named its primary way to build with Gemini going forward. Along the way you'll:

  • Make your first call and read back a structured response
  • Use server-side state to hold a multi-turn conversation without resending history
  • Wire up function calling so the model can trigger your own code
  • Combine a built-in tool (Google Search) with your own function in a single request
  • Kick off a long-running task in the background and poll for the result

Each exercise stands alone, so if you only have 20 minutes you can stop after Exercise 2 and still have something useful.


Why this matters (the 5-minute business case)

If you only ever read one section before handing this to your team, read this one.

The shift is from "prompt-in, text-out" to "stateful, agentic workflows." For two years most enterprise AI work has been built on a stateless request/response model: you send the entire conversation every time, you manage history yourself, and anything long-running has to be bolted on with your own queues and storage. The Interactions API moves that plumbing to Google's servers. That changes the cost and risk profile of three things businesses actually care about:

  1. Time-to-build for agents. A single API call can now provision a remote sandbox where an agent reasons, runs code, browses the web, and manages files (Google calls these Managed Agents). Work that used to be a multi-week orchestration project becomes a configuration choice. For a business, that compresses the gap between "we have an idea for an agent" and "we have a prototype in front of a customer."
  2. Operational simplicity and observability. Conversation state lives server-side and every action — a thought, a tool call, a result, the final answer — is a typed, inspectable step. For risk, compliance, and support teams, "show me exactly what the system did and why" stops being a custom logging project and becomes a property of the platform. (Note the flip side, covered in the lab: stored interactions are retained — 55 days on paid tier, 1 day on free tier — which is a data-governance decision you should make deliberately, not by default.)
  3. Cost control as a dial, not a rewrite. Flex and Priority tiers let you trade latency for cost (Flex advertises ~50% cost reduction), and continuing a conversation by reference (rather than resending it) improves cache hit rates. These are levers a finance-conscious engineering lead can pull without re-architecting.

The honest caveats, because pragmatic adoption means naming them:

  • At GA the API is the recommended path for new projects. The older generateContent API remains fully supported and is still Google's recommendation for stable production deployments. Don't rip out something that works.
  • It is a single-vendor interface. The patterns you learn here (typed steps, server-side state, tool combination) are broadly transferable, but the SDK calls are Google-specific. Weigh that against your portability requirements.
  • Capabilities and schemas have changed during the beta and may continue to evolve. Pin your SDK version and watch the changelog.

A useful way to frame it for a non-technical stakeholder: "Google just moved a lot of the 'glue code' that every AI team was writing — conversation memory, tool orchestration, background jobs — into the platform itself. That's less code for us to own, and faster prototypes, in exchange for tying that layer more tightly to Google."


Prerequisites

You needNotes
Python 3.9+python3 --version to check
A Gemini API keyFree to create at aistudio.google.com/apikey
Terminal comfortYou'll run a handful of scripts
~$0The free tier is enough for this lab. Exercise 5 (Deep Research) may require a paid-tier key; an alternative is provided.
A note on the free tier: stored interactions are retained for only 1 day on the free tier (vs. 55 days on paid). That's fine for a lab. Just know that if you come back tomorrow, the interaction IDs you saved today may have expired.

Setup (5 minutes)

1. Create a project folder and virtual environment.

bash
mkdir interactions-lab && cd interactions-lab
python3 -m venv .venv
source .venv/bin/activate      # Windows: .venv\Scripts\activate

2. Install the SDK. The Interactions API requires google-genai version 1.55.0 or newer.

bash
pip install -U "google-genai>=1.55.0"

3. Set your API key as an environment variable. The SDK picks up GEMINI_API_KEY automatically.

bash
export GEMINI_API_KEY="your-key-here"     # Windows (PowerShell): $env:GEMINI_API_KEY="your-key-here"

4. Verify everything is wired up. Create check.py:

python
from google import genai

client = genai.Client()  # reads GEMINI_API_KEY from the environment

interaction = client.interactions.create(
    model="gemini-3-flash-preview",
    input="Reply with exactly: setup OK",
)

print(interaction.output_text)

Run it:

bash
python check.py

If you see setup OK (or close to it), you're ready. If you get an authentication error, double-check the environment variable is set in the same terminal session you're running from.

`output_text` is your friend. The API returns a full timeline of steps, but the SDK gives you convenience properties — output_text, output_image, output_audio — so you don't have to walk the steps for simple cases. We'll look under the hood in Exercise 1.

Exercise 1 — Your first call and the anatomy of an Interaction (10 min)

Goal: Understand what actually comes back from interactions.create, then use server-side state to continue a conversation without resending it.

1a. Make a call and inspect the steps

The mental model that matters: an `Interaction` is one complete turn, and it contains a chronological list of typed steps. Steps can be the model's thought, a function_call, a function_result, and the final model_output. Let's see them.

Create ex1_steps.py:

python
from google import genai

client = genai.Client()

interaction = client.interactions.create(
    model="gemini-3-flash-preview",
    input="In one sentence, what is the Interactions API?",
)

# The convenience way:
print("--- output_text ---")
print(interaction.output_text)

# The under-the-hood way — walk the typed steps:
print("\n--- steps ---")
for step in interaction.steps:
    print(f"step type: {step.type}")
    if step.type == "model_output":
        for block in step.content:
            if block.type == "text":
                print(f"   text: {block.text}")

# Save this ID — we'll use it to continue the conversation:
print(f"\ninteraction id: {interaction.id}")

Run it and note two things: (1) output_text gave you the answer in one line, and (2) the steps view shows you the structure — this is what makes complex agent runs debuggable. Copy the interaction ID it prints; you'll paste it into the next step.

Business callout: That typed-step timeline is the feature your audit and support teams will care about most. When a customer asks "why did the assistant say that?", you can replay the exact sequence of thoughts, tool calls, and results rather than reconstructing it from logs.

1b. Continue the conversation with server-side state

In the old world you'd resend the whole conversation every turn. Here you pass previous_interaction_id and the server retrieves the history for you.

Create ex1_state.py (paste your saved ID into PREVIOUS_ID):

python
from google import genai

client = genai.Client()

PREVIOUS_ID = "paste-the-id-from-ex1_steps-here"

followup = client.interactions.create(
    model="gemini-3-flash-preview",
    input="Now explain that same thing to a CFO in one sentence.",
    previous_interaction_id=PREVIOUS_ID,
)

print(followup.output_text)

The model answers as if the first exchange just happened — because, on Google's servers, it did. You never resent the original question.

Watch out: previous_interaction_id carries only the conversation history. Settings like tools, system_instruction, and generation_config are scoped to each individual call. If you want a system instruction or a tool to apply on the follow-up, you must pass it again. This trips up almost everyone the first time.

Checkpoint: You can make a call, read both the simple and structured forms of the response, and continue a conversation by reference. That's the foundation; everything else builds on it.


Exercise 2 — Function calling: let the model trigger your code (12 min)

Goal: Give the model a tool it can decide to call, execute that tool yourself, and feed the result back for a natural-language answer.

This is the single most practical agentic pattern. The model never runs your code — it tells you what to run and with what arguments; you execute and return the result. Four steps: declare → call → execute → return.

2a. Declare a function and let the model call it

We'll build a tiny "order status" lookup — the kind of thing a support assistant needs.

Create ex2_functions.py:

python
import json
from google import genai

client = genai.Client()

# --- Step 1: Declare the function (a JSON schema the model can read) ---
get_order_status = {
    "type": "function",
    "name": "get_order_status",
    "description": "Look up the current status of a customer order by its ID.",
    "parameters": {
        "type": "object",
        "properties": {
            "order_id": {
                "type": "string",
                "description": "The order identifier, e.g. 'A-1024'",
            },
        },
        "required": ["order_id"],
    },
}

# --- This is YOUR real implementation (here, a fake lookup table) ---
def get_order_status_impl(order_id: str) -> dict:
    fake_db = {
        "A-1024": {"status": "shipped", "eta": "2026-06-25"},
        "A-1099": {"status": "processing", "eta": "2026-06-29"},
    }
    return fake_db.get(order_id, {"status": "not_found"})

# --- Step 2: Call the model with the tool available ---
interaction = client.interactions.create(
    model="gemini-3-flash-preview",
    input="Where's my order A-1024? When will it arrive?",
    tools=[get_order_status],
)

# --- Step 3: Find the function_call step and execute it ourselves ---
fc_step = next(s for s in interaction.steps if s.type == "function_call")
print(f"Model wants to call: {fc_step.name}({fc_step.arguments})")

result = get_order_status_impl(**fc_step.arguments)
print(f"Our function returned: {result}")

# --- Step 4: Send the result back so the model can phrase a reply ---
final = client.interactions.create(
    model="gemini-3-flash-preview",
    input=[
        {
            "type": "function_result",
            "name": fc_step.name,
            "call_id": fc_step.id,
            "result": [{"type": "text", "text": json.dumps(result)}],
        }
    ],
    tools=[get_order_status],
    previous_interaction_id=interaction.id,
)

print("\n--- Final answer ---")
print(final.output_text)

Run it. You'll see the model decide to call get_order_status, your code produce the data, and the model turn that data into a friendly sentence.

Two things to internalize: - The call_id (fc_step.id) ties your result back to the specific call. The SDK matches it for you, but passing it explicitly is the documented best practice — Gemini 3 generates a unique ID per call. - We re-passed tools= on the second call. Remember from Exercise 1: tools are call-scoped, so the model needs them again to understand the conversation.
Business callout: This is the bridge between an LLM and your systems of record — CRMs, inventory, ticketing, billing. The model handles language and intent; your code remains the system of authority. That separation is exactly what keeps a deployment governable: the model can't invent an order status, it can only ask your function for one.

2b. (Optional, +2 min) Try a question with no matching tool

Change the input to "What's your refund policy?" and run again. The model answers directly without calling the function, because nothing matched. That's the default auto tool-choice behavior — the model decides whether a tool is even needed. (You can force the issue with generation_config={"tool_choice": "any"} if you ever need to guarantee a call.)

Checkpoint: You've completed a full function-calling round trip. This pattern — declare, call, execute, return — is 80% of practical agent building.


Exercise 3 — Combine a built-in tool with your own function (10 min)

Goal: In one request, let the model use Google Search (a built-in tool) and your custom function together.

Real questions are rarely "all internal" or "all external." "What's the weather where my order is shipping, and is it on time?" needs the open web and your database. Gemini 3 models can mix built-in tools with your functions out of the box.

Create ex3_tool_combo.py:

python
import json
from google import genai

client = genai.Client()

get_order_status = {
    "type": "function",
    "name": "get_order_status",
    "description": "Look up the current status and destination city of a customer order.",
    "parameters": {
        "type": "object",
        "properties": {
            "order_id": {"type": "string", "description": "Order ID, e.g. 'A-1024'"},
        },
        "required": ["order_id"],
    },
}

def get_order_status_impl(order_id: str) -> dict:
    fake_db = {
        "A-1024": {"status": "shipped", "destination": "Denver, Colorado", "eta": "2026-06-25"},
    }
    return fake_db.get(order_id, {"status": "not_found"})

# Mix a built-in tool with our custom one in a single tools list:
tools = [
    {"type": "google_search"},   # built-in
    get_order_status,            # custom
]

interaction = client.interactions.create(
    model="gemini-3-flash-preview",
    input="My order A-1024 — where is it headed, and what's the weather there right now?",
    tools=tools,
)

# The model may call our function; Google Search runs server-side automatically.
for step in interaction.steps:
    if step.type == "function_call":
        print(f"Custom function call: {step.name}({step.arguments})")
        result = get_order_status_impl(**step.arguments)

        final = client.interactions.create(
            model="gemini-3-flash-preview",
            previous_interaction_id=interaction.id,
            tools=tools,
            input=[
                {
                    "type": "function_result",
                    "name": step.name,
                    "call_id": step.id,
                    "result": [{"type": "text", "text": json.dumps(result)}],
                }
            ],
        )
        print("\n--- Final answer ---")
        print(final.output_text)
        break
else:
    # If the model answered without needing our function:
    print(interaction.output_text)

Run it. The model uses your function to find the destination city (Denver) and Google Search to look up current weather there — then blends both into one answer.

Note: Passing previous_interaction_id on the follow-up automatically circulates the built-in Google Search context from the first turn, so you don't lose what Search found. This is the documented pattern for keeping built-in-tool context across turns.
Business callout: "Grounding" — forcing answers to cite real sources rather than the model's memory — is one of the biggest levers for trust in enterprise AI. Here you get two kinds of grounding at once: the open web (Search) and your own data (your function). That combination is hard to build from scratch and nearly free here.

Checkpoint: You can compose built-in and custom tools. This is where "chatbot" becomes "assistant that actually does things."


Exercise 4 — Background execution for long-running work (10 min)

Goal: Start a task that may take a while, get an ID back immediately, and poll for completion — without holding a connection open.

Some work is slow: deep reasoning, research across many sources, large generations. Blocking your app (or a user's browser) on it is bad design. The Interactions API lets you set background=True; the server runs the work asynchronously and you check back.

4a. The background pattern

Create ex4_background.py:

python
import time
from google import genai

client = genai.Client()

# Kick off a non-trivial task in the background. We get an ID back right away.
interaction = client.interactions.create(
    model="gemini-3-flash-preview",
    input=(
        "Draft a one-page internal brief explaining the business case for adopting "
        "a stateful agent API. Cover benefits, risks, and a recommended first step."
    ),
    background=True,
)

print(f"Started background interaction: {interaction.id}")
print(f"Initial status: {interaction.status}")

# Poll until it's done. In a real app this would be a webhook or a job that
# checks periodically — not a tight loop.
while interaction.status in ("queued", "in_progress", "running"):
    time.sleep(2)
    interaction = client.interactions.get(name=interaction.id)
    print(f"  ...status: {interaction.status}")

print("\n--- Result ---")
print(interaction.output_text)

Run it. You'll see the status print as the job moves toward completion, then the finished brief.

What just happened, architecturally: the call returned immediately with an ID instead of waiting. That's the difference between "user stares at a spinner for 40 seconds" and "user gets told 'we're working on it' and gets a notification when it's ready." Note that background=True requires store=true (the default) — the server has to persist the job to run it asynchronously.
Production note: Polling in a while loop is fine for a lab. In a real system you'd use webhooks so the server notifies you when the job finishes, instead of you repeatedly asking.

4b. (Optional) Point it at the Deep Research agent

The same background pattern powers Google's heavier agents. One of the headline GA additions is Deep Research — an agent that plans, gathers from many sources, and produces a structured report. It's invoked exactly like a model, but with an agent ID instead of a model ID:

python
# Requires a paid-tier key for the Deep Research agent.
interaction = client.interactions.create(
    model="deep-research-preview-04-2026",   # an AGENT id, not a model id
    input="Compare three approaches to grounding LLM outputs for enterprise use. Cite sources.",
    background=True,
)
print(interaction.id, interaction.status)
# ...then poll with client.interactions.get(name=interaction.id) as in 4a.
If you're on the free tier: skip running 4b — the Deep Research agents may not be available to you. The pattern is the takeaway: the same `background=True` + poll code works whether you pass a model ID or an agent ID. That uniformity ("models and agents through one interface") is the whole point of the GA release.
Business callout: Background execution is what makes "agentic" economically viable. A 3-minute research task that would time out a synchronous request becomes a fire-and-forget job. It also changes the UX conversation with stakeholders: you're no longer designing around "how fast can the model answer" but "how do we notify the user when the work is done" — a much healthier framing for ambitious automation.

Checkpoint: You can offload slow work and you understand that models and agents share one calling convention.


Clean up (1 min)

Stored interactions count toward retention windows. If you created anything you'd rather not leave on Google's servers (especially with a paid key's 55-day retention), delete it by ID:

python
from google import genai
client = genai.Client()
client.interactions.delete(name="paste-an-interaction-id-here")

You can only delete interactions whose ID you know, and everything is auto-deleted after the retention period regardless.


Wrap-up

You went from zero to a working understanding of Google's primary Gemini interface. Specifically, you can now:

  • Make a call and read responses both the easy way (output_text) and the structured way (typed steps)
  • Hold a conversation using server-side state (previous_interaction_id) instead of resending history
  • Run the full function-calling loop — declare, call, execute, return
  • Combine a built-in tool (Google Search) with your own function for dual-grounded answers
  • Offload slow work with background=True and poll for the result, using the same convention for models and agents

The one idea to carry forward: the Interactions API's bet is that conversation state, tool orchestration, and background execution belong in the platform, not in your codebase. Whether that's the right trade for a given project depends on your portability needs and how much glue code you're tired of maintaining — but the patterns you practiced here (typed steps, tool combination, async jobs) are how agentic systems are converging across the industry, regardless of vendor.


Stretch challenge (for the labs series)

Combine all four skills into one small program: a "shipment concierge" that

  1. takes an order ID,
  2. calls your get_order_status function to find status and destination,
  3. uses Google Search to check weather and any local shipping advisories at that destination,
  4. runs the synthesis as a background=True task, and
  5. continues the conversation (via previous_interaction_id) so a user can ask a follow-up like "should I be worried about the delivery date?"

If you publish your solution as the next lab in the series, the natural follow-on topics are streaming (rendering tokens and tool steps live as they arrive) and structured output (forcing the final answer into a JSON schema your UI can consume directly).


Troubleshooting

SymptomLikely cause / fix
Authentication / 401 errorGEMINI_API_KEY not set in the current terminal session. Re-run the export line.
AttributeError: 'Client' object has no attribute 'interactions'SDK too old. pip install -U "google-genai>=1.55.0".
Model never calls your functionMake the function description and parameter descriptions more specific, or force it with generation_config={"tool_choice": "any"}.
previous_interaction_id follow-up "forgot" a tool or system promptExpected — those are call-scoped. Re-pass tools / system_instruction on every call.
Background job never leaves queuedConfirm you didn't set store=False (incompatible with background=True).
Deep Research agent (4b) returns an access errorIt may require a paid-tier key. The pattern still stands; run 4a instead.
Yesterday's interaction IDs return "not found"Retention expired — 1 day on free tier, 55 days on paid.

References

All technical details in this lab are drawn from Google's official announcement and documentation:

A note on accuracy for your readers: the Interactions API evolved during its beta and Google notes schemas may continue to change. Before publishing, it's worth running each script once against the current SDK and adding a "verified on google-genai version X.Y.Z, June 2026" line. Code that runs builds far more trust than code that merely looks right.
WG

Written by Wes Goldwater, Director of Engineering at Prosigliere.

Hands-on cloud & pragmatic AI from Goldwater.dev.