With ADK, you can build agents that autonomously interact with your data by generating and executing queries directly within BigQuery. This unlocks some really exciting opportunities, but also raises some critical questions. How can you monitor what the agent is doing with your data? What if the agent deletes your data, or runs a super expensive query ? Can you really trust an agent with your valuable data?
Yes, you can … so long as you have the right safeguards in place. This post outlines some of the guardrails you can implement to mitigate the risks of unintended actions, unexpected costs and opaque operations, making your agents more transparent, predictable and trustworthy.
Let’s dive in. While some of these tips are specific to BigQuery or ADK, many of the underlying concepts hold true for other agentic frameworks and data platforms.
The foundation of having your agent do what is expected lies in its system instructions. These instructions define the agent’s persona, its goals, its limitations, and constraints — what it should not do.
You are a data analysis agent with access to several BigQuery tools. Your primary function is to query BigQuery using the provided tools to answer user questions.
Adhere to the following guidelines:
- Data Access - You are permitted to access data only within the following datasets and tables ...
- Data Manipulation (DML) Operations - You are [ALLOWED / NOT ALLOWED] to perform DML operations ...
- Query Optimization - Never use SELECT *. Always explicitly list only the columns required for the user's request ...
- Results - Unless explicitly requested by the user, always include a LIMIT clause to restrict the number of rows returned...- Examples ...
Your agent’s instructions must explicitly cover:
- What datasets and tables it can access
- What (if any) DML operations (i.e. UPDATE, DELETE) it can perform
- How to optimize the queries it generates (avoid SELECT * for example)
- How much data it should return (LIMIT)
- And much more — provide examples, specify the output format, tell the agent how to handle errors and give clear expectations around hallucinations (don’t include made-up gibberish in the queries!)
LLM models are improving at a rapid pace, but even still, just because you ask nicely, there’s no guarantee that your agent is going to follow all of these instructions, all the time. Fortunately, it’s a risk you don’t need to take, and you can add explicit checks to validate what your agent is going to do, before it does it.
Imagine a security checkpoint before any action is taken. ADK’s Callbacks allow you to execute functions that are called before or after a tool or model executes a task. In these functions, you can:
- Validate SQL queries: Manually check for forbidden statements like DELETE, DROP, or UPDATE, or the omission of LIMIT statements.
- Enforce business rules: Make sure queries adhere to specific data policies, for example, it could manually check the query does not include tables with sensitive data.
- Log intent: Record what the agent intends to do before it does it, which will be super valuable when the need for traceability comes along (and trust me, it will come).
#basic psuedo code exampledef validate_sql_callaback(tool: BaseTool, args: Dict[str, Any], tool_context: ToolContext) -> Optional[Dict]:
if tool.name == 'execute_sql' and 'delete' in args.get('sql_query', '').lower():
return
{
"result": "Tool execution was blocked due to forbidden delete statement!"
}
return None
root_agent = LlmAgent(
model="gemini-2.5-flash",
name="bigquery_agent",
instruction=""" You are a data analysis agent with access to several BigQuery tools...
""",
tools=[bigquery_toolset],
before_tool_callback=validate_sql_callaback
)
Does your agent really need to generate SQL from scratch? Giving an agent the freedom to generate and execute any query it likes opens the door to potential SQL injection. An alternative approach is to provide the agent with query templates that have parameters. The agent is then only responsible for providing the values for those parameters, rather than constructing the entire query string. SQL parameterization significantly limits its ability to inject malicious or unintended SQL:
from google.cloud import bigquery# Construct a BigQuery client object.
client = bigquery.Client()
query = """
SELECT order_id, transaction_amount
FROM `your_project.your_dataset.transactions`
WHERE region = @target_region
AND transaction_amount >= @min_amount
LIMIT 10;
"""
job_config = bigquery.QueryJobConfig(
query_parameters=[
bigquery.ScalarQueryParameter("target_region", "STRING", "Europe"),
bigquery.ScalarQueryParameter("min_amount", "NUMERIC", 500.0),
]
)
results = client.query_and_wait(
query, job_config=job_config)
These query templates can be wrapped as custom function tools in ADK, or be hosted in the MCP toolbox for Databases (see tip 8!).
Once you’ve limited the SQL that an agent can generate, you’ll want to start thinking about controlling its execution.
Both humans and agents are capable of executing runaway queries that unintentionally rack up a massive bill (whoopsie-daisy!). Before the agent commits a potentially expensive query, you can perform a dry run by setting the dry_run flag to true in BigQuery’s QueryJobConfig:
job_config = bigquery.QueryJobConfig(dry_run=True, use_query_cache=False)# Start the query, passing in the extra configuration.
query_job = client.query(sql_query, job_config=job_config,
# A dry run query completes immediately.
print("This query will process {} bytes.".format(query_job.total_bytes_processed))
This doesn’t actually execute the query but returns an estimate of the bytes that would be processed. You can integrate this into your callback context or agent logic to take action if the estimate is above a certain threshold.
If you have a specific threshold in mind, you can set the maximum_bytes_billed parameter in your QueryJobConfig that will ensure any query that is estimated to process more data than this threshold will fail (and you won’t be charged). This acts as a hard stop.
If you have your own custom function that executes your SQL in BigQuery, you can provide this parameter as part of the BigQuery JobConfiguration:
job_config = bigquery.QueryJobConfig(
maximum_bytes_billed=10_000_000_000 # 10 GB in bytes
)try:
query_job = client.query(sql_query, job_config=job_config,
results = query_job.result()
print(f"Query completed successfully. Processed query_job.total_bytes_processed} bytes.")
for row in results:
print(row)
except Exception as e:
print(f"Query failed: {e}")
# Check for specific error messages related to maxBytesBilled
Alternatively, if you’re using the execute_sql tool from BigQuery’s pre-built first-party toolset for ADK and MCP, you get these controls (and more) built-in for free:
- Strict Write Controls: You can set “write modes” to precisely dictate what kind of database operations are allowed.
- Prevents Unwanted Changes: In BLOCKED mode, only INSERT statements are allowed, DDL or DML statements are forbidden. In ‘protected’ mode, DDL/DML is allowed, but only to temporary tables in BigQuery.
- Limits Result Size: The number of rows returned are automatically capped, keeping memory usage in check.
from google.adk.tools.bigquery import BigQueryToolsetbigquery_toolset = BigQueryToolset(my_bq_credentials)
This toolset is built and maintained by Google, and is regularly updated with new features and tools. It’s definitely worth exploring what they can offer.
Your agent should now produce optimized SQL, with built-in cost limits and restrictions on what types of queries it can execute.
We can now take a step back and restrict your agents’ fundamental interactions with BigQuery itself, controlling not just what queries it runs and how much it spends, but precisely what parts of your BigQuery environment it can even access.
Identity and Access Management (IAM) is your primary gatekeeper to BigQuery and you should always apply stringent IAM permissions with the principle of least privilege:
As your agents and applications scale, managing their individual BigQuery credentials and access needs can get tricky. What if you need to change the authentication method? You’ll have to do this for every agent. This tight coupling can lead to a large operational overhead and increased risk.
8. Use MCP Toolbox for Databases
The MCP (Model Context Protocol) Toolbox for Databases is an open-source server that centralizes how you host and manage toolsets. This effectively decouples your agent applications from interacting directly with BigQuery. Instead of each agent needing its own set of potentially broad BigQuery credentials distributed across multiple deployments, agents communicate solely with the MCP Toolbox. This centralization means your agents just need permission to talk to the MCP server, which then securely handles all BigQuery authentication and access. The result? A significantly reduced attack surface and simplified credential management.
The MCP Toolbox for Databases natively supports BigQuery’s pre-built toolset as well as SQL templates wrapped as custom tools. While MCP is still relatively new, it’s advancing at pace, and should definitely be on your radar if you’re looking to productionize and scale your agentic applications.
And finally, let’s shift our focus to operational oversight.
To understand why you got a particular result, you need to first understand what your agent did. Meticulously logging all agent actions, queries, responses, and errors creates an audit trail that will be vital for forensics, compliance, and understanding agent behavior over time.
You can add logging throughout your ADK application. It can be particularly effective to implement logging within your tools and callbacks (see tip 2), capturing key context before, during and after an agent or tool call.
Jobs executed in BigQuery are captured in audit logs and can be exported to BigQuery for analysis, providing a clear record of what ran, when, and by which user or service account. BigQuery also provides detailed job metadata, including bytes processed, slot usage, and full query execution plans, offering insights into performance and cost for every query, highlighting areas for potential optimization.
For local development, the ADK Web UI has a neat Trace tab for debugging and evaluating agent behaviour. For agents deployed to Vertex AI Agent Engine, you can enable Cloud Trace to view traces and analyze agent actions, giving you a window into the agent’s decision-making process. You can also set up proactive monitoring and alerting on key metrics like latency or error rates to get real time insights into unexpected behaviour, allowing you to react and debug in the moment.
10. Human Oversight and Approval Workflows
For critical or sensitive actions, you may want to think about introducing a “human-in-the-loop” for certain key tasks. For example, if an agent wants to delete or update data in an important table, or run a very large, potentially costly query, your workflow could include a step that requires a person to review and approve the action before it actually happens.
There is no built-in ‘human-agent’ type in ADK, but you can implement your own custom integration pattern as outlined here.
No single guardrail is a silver bullet. When used together, however, these complementary approaches build a robust defense. Each layer adds an extra level of protection, making your BigQuery agents more reliable, robust, and trustworthy. By intentionally implementing these safeguards, you can develop your AI agents with confidence, knowing your data is well-protected.
Confidence boosted, and ready to give this a try?
This isn’t an exhaustive list, and thankfully, the techniques for safeguarding data are ever evolving. Please do share any additional safeguarding tips you’ve found effective in the comments!
Source Credit: https://medium.com/google-cloud/bigquery-meets-adk-10-tips-to-safeguard-your-data-and-wallet-from-agents-8c8ea72a9d4e?source=rss—-e52cf94d98af—4
