<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Claix</title>
    <description>The latest articles on DEV Community by Claix (@claix).</description>
    <link>https://dev.to/claix</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4123141%2Fbe99e4d4-5f75-4838-8c4e-5d10454a693b.png</url>
      <title>DEV Community: Claix</title>
      <link>https://dev.to/claix</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/claix"/>
    <language>en</language>
    <item>
      <title>Read-Only Postgres Role for AI Agents: 7 Gaps to Close</title>
      <dc:creator>Claix</dc:creator>
      <pubDate>Sat, 31 Oct 2026 15:58:53 +0000</pubDate>
      <link>https://dev.to/claix/read-only-postgres-role-for-ai-agents-7-gaps-to-close-25m9</link>
      <guid>https://dev.to/claix/read-only-postgres-role-for-ai-agents-7-gaps-to-close-25m9</guid>
      <description>&lt;p&gt;A read-only Postgres role for AI agents is the right first step. It is also not enough on its own.&lt;br&gt;
Most guides stop at GRANT SELECT. We ran the usual setup on PostgreSQL 16, tried to get around it the way a manipulated agent or a careless query would, and found seven places where "read-only" doesn't mean what people assume. Each one comes with the SQL to reproduce it and the fix.&lt;br&gt;
Quick answer&lt;br&gt;
A read-only Postgres role for an AI agent should be a dedicated login role with no superuser or BYPASSRLS, SELECT granted only on the columns the agent needs, and timeouts set. But default_transaction_read_only and statement_timeout are defaults that a session can change, a SECURITY DEFINER function can still write, and anything you grant can still be read by a manipulated agent. The real barrier is what the role is allowed to do, so grant as little as possible and never give the agent a raw SQL session.&lt;br&gt;
In this post&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;How we tested&lt;/li&gt;
&lt;li&gt;The baseline setup&lt;/li&gt;
&lt;li&gt;Gap 1: the read-only setting is a default, not a lock&lt;/li&gt;
&lt;li&gt;Gap 2: your timeout can be switched off&lt;/li&gt;
&lt;li&gt;Gap 3: a SECURITY DEFINER function can write for you&lt;/li&gt;
&lt;li&gt;Gap 4: table names stay visible&lt;/li&gt;
&lt;li&gt;Gap 5: SELECT * fails, and the error blames the table&lt;/li&gt;
&lt;li&gt;Gap 6: row-level security with no policy returns nothing&lt;/li&gt;
&lt;li&gt;Gap 7: everything you grant can be read&lt;/li&gt;
&lt;li&gt;The hardened setup in one block&lt;/li&gt;
&lt;li&gt;FAQ
How we tested
Everything below ran on PostgreSQL 16.15 in a clean database, with each statement sent from a normal login role. The results are copied from the terminal. Nothing needs an extension, and you can paste each block into psql and compare.
The sample schema has three tables: customers, orders and secrets. The agent should reach only some columns of the first two, and nothing in the third.
The baseline setup
This is the setup most guides describe, plus the settings people add to make it "read-only".
CREATE TABLE public.customers (id serial PRIMARY KEY, name text, email text);
CREATE TABLE public.orders (
id serial PRIMARY KEY, customer_id int REFERENCES customers(id),
status text, total numeric, internal_notes text
);
CREATE TABLE public.secrets (id serial PRIMARY KEY, token text);&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;CREATE ROLE claix_agent WITH LOGIN PASSWORD 'use-a-secret-manager'&lt;br&gt;
  NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOREPLICATION NOBYPASSRLS;&lt;/p&gt;

&lt;p&gt;GRANT CONNECT ON DATABASE mydb TO claix_agent;&lt;br&gt;
GRANT USAGE ON SCHEMA public TO claix_agent;&lt;br&gt;
GRANT SELECT (id, name) ON public.customers TO claix_agent;&lt;br&gt;
GRANT SELECT (id, customer_id, status, total) ON public.orders TO claix_agent;&lt;/p&gt;

&lt;p&gt;ALTER ROLE claix_agent SET default_transaction_read_only = on;&lt;br&gt;
ALTER ROLE claix_agent SET statement_timeout = '2s';&lt;/p&gt;

&lt;p&gt;Two things to avoid at this stage. Don't use GRANT SELECT ON ALL TABLES, which also covers tables you didn't mean to expose. And don't use the built-in pg_read_all_data role for an agent: it grants read access to every table in the database, which is far more than an agent needs.&lt;br&gt;
Gap 1: the read-only setting is a default, not a lock&lt;br&gt;
The assumption: default_transaction_read_only = on means the role can't write.&lt;br&gt;
What happens: it means new transactions start read-only. The session can change that. To see it, we made a second role that does have INSERT and the same read-only default:&lt;br&gt;
INSERT INTO orders(customer_id, status, total) VALUES (1, 'x', 1);&lt;br&gt;
ERROR:  cannot execute INSERT in a read-only transaction&lt;/p&gt;

&lt;p&gt;SET default_transaction_read_only = off;&lt;br&gt;
INSERT INTO orders(customer_id, status, total) VALUES (1, 'x', 1);&lt;br&gt;
-- succeeded&lt;br&gt;
The same happened with BEGIN READ ONLY; followed by SET TRANSACTION READ WRITE; as the first statement: the write went through.&lt;br&gt;
What stopped the write in the real agent role was something else. After SET default_transaction_read_only = off;, an INSERT from claix_agent failed with:&lt;br&gt;
ERROR:  permission denied for table orders&lt;br&gt;
The fix: treat the setting as a safety net and the privileges as the barrier. Grant SELECT only. If you can, connect the agent to a read replica, because a standby server rejects writes whatever the session sets (we didn't test this one).&lt;br&gt;
Gap 2: your timeout can be switched off&lt;br&gt;
The assumption: ALTER ROLE ... SET statement_timeout = '2s' caps every query.&lt;br&gt;
What happens: it sets the starting value. The session can raise it or remove it:&lt;br&gt;
SHOW statement_timeout;&lt;br&gt;
 2s&lt;/p&gt;

&lt;p&gt;SELECT pg_sleep(3);&lt;br&gt;
ERROR:  canceling statement due to statement timeout&lt;/p&gt;

&lt;p&gt;SET statement_timeout = 0;&lt;br&gt;
SELECT pg_sleep(3);&lt;br&gt;
-- ran to completion&lt;br&gt;
The fix: the timeout protects you only if the agent can't send a SET. So never give the agent a raw SQL session. Your code should run a single validated SELECT, set the limits itself for each transaction (SET LOCAL statement_timeout = '5s') and add its own timeout around the call. A pooler or proxy that cancels long queries is a second layer, and you should check its documentation for the exact setting.&lt;br&gt;
Gap 3: a SECURITY DEFINER function can write for you&lt;br&gt;
The assumption: a role with only SELECT can't change data.&lt;br&gt;
What happens: functions declared SECURITY DEFINER run with the privileges of their owner, and by default Postgres lets everyone execute functions. We created one that inserts into secrets, a table claix_agent can't touch:&lt;br&gt;
CREATE FUNCTION public.archive_orders() RETURNS void&lt;br&gt;
LANGUAGE sql SECURITY DEFINER AS $$&lt;br&gt;
  INSERT INTO public.secrets(token) VALUES ('written-by-function')&lt;br&gt;
$$;&lt;br&gt;
With the read-only default on, the call failed. After the session switched it off (gap 1), it worked, and secrets went from one row to two.&lt;br&gt;
The fix: find these functions and remove public execute access from the ones that write:&lt;br&gt;
SELECT p.oid::regprocedure&lt;br&gt;
FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace&lt;br&gt;
WHERE n.nspname = 'public' AND p.prosecdef;&lt;/p&gt;

&lt;p&gt;REVOKE EXECUTE ON FUNCTION public.archive_orders() FROM PUBLIC;&lt;br&gt;
After the revoke, the same call returned permission denied for function archive_orders. Give EXECUTE back to the roles that need it. You can also run ALTER DEFAULT PRIVILEGES IN SCHEMA public REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC; so that future functions aren't public by default, but that affects every function created afterwards by the same role, so check your application first.&lt;br&gt;
Gap 4: table names stay visible&lt;br&gt;
The assumption: if the role can't read a table, it can't see it.&lt;br&gt;
What happens: it can't read the contents, but the catalog still lists the name:&lt;br&gt;
SELECT tablename FROM pg_tables WHERE schemaname = 'public';&lt;br&gt;
 customers&lt;br&gt;
 orders&lt;br&gt;
 secrets&lt;/p&gt;

&lt;p&gt;SELECT table_name FROM information_schema.tables WHERE table_schema = 'public';&lt;br&gt;
 customers&lt;br&gt;
 orders&lt;/p&gt;

&lt;p&gt;SELECT * FROM secrets;&lt;br&gt;
ERROR:  permission denied for table secrets&lt;br&gt;
information_schema shows only what the role has privileges on. pg_catalog shows everything.&lt;br&gt;
The fix: don't put anything sensitive in a table or column name. And when you build the description of your schema for an agent, read it with the agent's own role through information_schema, so the agent is told about the tables it can use and nothing else.&lt;br&gt;
Gap 5: SELECT * fails, and the error blames the table&lt;br&gt;
The assumption: with a column-level grant, SELECT * returns the allowed columns.&lt;br&gt;
What happens: it fails, and the message names the table, not the column:&lt;br&gt;
SELECT * FROM orders;&lt;br&gt;
ERROR:  permission denied for table orders&lt;/p&gt;

&lt;p&gt;SELECT id, status, total FROM orders ORDER BY id LIMIT 2;&lt;br&gt;
 1|shipped|10&lt;br&gt;
 2|pending|20&lt;/p&gt;

&lt;p&gt;SELECT internal_notes FROM orders;&lt;br&gt;
ERROR:  permission denied for table orders&lt;br&gt;
That is useful: an agent can't get a column you didn't grant by accident. It is also confusing when you debug, because the table looks denied even though only one column is.&lt;br&gt;
The fix: make your agent list its columns, and reject SELECT * in your validator so the failure is a clear message from your code and not a database error. A view with only the safe columns is a simple alternative:&lt;br&gt;
CREATE VIEW public.order_status WITH (security_invoker = true) AS&lt;br&gt;
  SELECT id, status FROM public.orders;&lt;/p&gt;

&lt;p&gt;GRANT SELECT ON public.order_status TO claix_agent;&lt;br&gt;
The security_invoker option (PostgreSQL 15 and later) makes the view use the privileges and row-level security of the caller, not the view's owner.&lt;br&gt;
Gap 6: row-level security with no policy returns nothing&lt;br&gt;
The assumption: enabling row-level security on a table limits what the agent sees.&lt;br&gt;
What happens: if the role is not the owner and no policy applies to it, it sees zero rows. That fails safe, and it looks like an empty table:&lt;br&gt;
ALTER TABLE public.customers ENABLE ROW LEVEL SECURITY;&lt;/p&gt;

&lt;p&gt;SELECT count(*) FROM customers;   -- as claix_agent&lt;br&gt;
 0&lt;/p&gt;

&lt;p&gt;CREATE POLICY agent_read ON public.customers FOR SELECT TO claix_agent USING (true);&lt;/p&gt;

&lt;p&gt;SELECT count(*) FROM customers;&lt;br&gt;
 2&lt;br&gt;
The fix: if you use row-level security, write a policy for the agent role with the narrowest condition that works, and check that the role is not the table owner and does not have BYPASSRLS. If an agent suddenly "finds nothing", look at the policies first.&lt;br&gt;
Gap 7: everything you grant can be read&lt;br&gt;
The assumption: once the role is read-only, the data is safe.&lt;br&gt;
What happens: a read-only role protects what it can't reach. It doesn't protect what you gave it. Instructions hidden in a user message, a document or a database field can change what an agent asks for, and the role will answer any query for data it was granted. Supabase's own documentation names prompt injection as the main risk of connecting an agent to your data, and says its MCP server is designed for development and testing.&lt;br&gt;
A role also can't tell a useful query from a pointless or expensive one, or a correct query from one that is valid and wrong, and it doesn't separate one person's rows from another's.&lt;br&gt;
The fix: grant less, and add layers outside the database:&lt;br&gt;
• Validate every query with a real SQL parser before it runs: one SELECT, approved tables, an allowlist of functions.&lt;br&gt;
• Add a row limit, a timeout and a cost check.&lt;br&gt;
• Treat query results as untrusted data.&lt;br&gt;
• Log every request: the question, the query and the result.&lt;br&gt;
• Keep per-user rules in row-level security or in your application.&lt;br&gt;
The hardened setup in one block&lt;br&gt;
This is the baseline plus the fixes that belong in the database. We ran the whole block on a clean cluster and checked that granted columns work and others don't.&lt;br&gt;
-- 1. A dedicated role, not your app user and not postgres&lt;br&gt;
CREATE ROLE claix_agent WITH LOGIN PASSWORD 'use-a-secret-manager'&lt;br&gt;
  NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOREPLICATION NOBYPASSRLS&lt;br&gt;
  CONNECTION LIMIT 10;&lt;br&gt;
GRANT CONNECT ON DATABASE mydb TO claix_agent;&lt;br&gt;
GRANT USAGE ON SCHEMA public TO claix_agent;&lt;/p&gt;

&lt;p&gt;-- 2. Columns, not whole tables&lt;br&gt;
GRANT SELECT (id, name) ON public.customers TO claix_agent;&lt;br&gt;
GRANT SELECT (id, customer_id, status, total) ON public.orders TO claix_agent;&lt;/p&gt;

&lt;p&gt;-- 3. Guardrails (defaults the session can change)&lt;br&gt;
ALTER ROLE claix_agent SET default_transaction_read_only = on;&lt;br&gt;
ALTER ROLE claix_agent SET statement_timeout = '5s';&lt;br&gt;
ALTER ROLE claix_agent SET idle_in_transaction_session_timeout = '10s';&lt;/p&gt;

&lt;p&gt;-- 4. Find functions that run with their owner's privileges&lt;br&gt;
SELECT p.oid::regprocedure AS security_definer_function&lt;br&gt;
FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace&lt;br&gt;
WHERE n.nspname = 'public' AND p.prosecdef;&lt;/p&gt;

&lt;p&gt;-- 5. Remove public execute access from any that write&lt;br&gt;
REVOKE EXECUTE ON FUNCTION public.archive_orders() FROM PUBLIC;&lt;br&gt;
-- GRANT EXECUTE ON FUNCTION public.archive_orders() TO app_user;&lt;br&gt;
Then add what a database role can't: validation, limits, logging, and a controlled way for the agent to ask.&lt;br&gt;
Where Claix fits&lt;br&gt;
Claix is a managed service between your agent and your database. You connect Postgres or Supabase with a read-only role like the one above, choose the tables and columns each agent can use, and write a short guide with your rules. Your agent sends questions to its own endpoint and gets typed answers back, so it never holds a database connection or sends raw SQL. Every request is traced.&lt;br&gt;
It doesn't replace the role. Claix connects with a role like this one, and what that role can read is still the ceiling. You can try it in the Sandbox and read the docs at claix.dev.&lt;br&gt;
FAQ&lt;br&gt;
Is a read-only user enough to secure a database for an AI agent? No. It is the first layer. A session can change settings such as default_transaction_read_only and statement_timeout, a SECURITY DEFINER function can still write, and a manipulated agent can read anything you granted. Add query validation, limits and logging.&lt;br&gt;
How do I create a read-only user in Postgres? Create a login role without superuser or BYPASSRLS, grant CONNECT on the database and USAGE on the schema, and grant SELECT only on the tables or columns it needs. The block in this post does that.&lt;br&gt;
Can a read-only Postgres user still modify data? It can if it can call a SECURITY DEFINER function that writes, or if the session lifts the read-only default and the role has write privileges. With only SELECT privileges and no public execute access to writing functions, it can't.&lt;br&gt;
How do I limit an AI agent to certain columns in Postgres? Use column-level grants, such as GRANT SELECT (id, status) ON orders TO claix_agent, or expose a view with only the safe columns.&lt;br&gt;
Should an AI agent connect to a read replica? It is a good extra layer, because a standby server rejects writes whatever the session sets, and it keeps heavy queries away from your main database. It doesn't replace limiting what the role can read.&lt;br&gt;
Does this work on Supabase? Yes. Supabase is Postgres, so the same SQL works in its SQL editor. Supabase's MCP server has a read-only option that runs queries as a read-only Postgres user, and its documentation says the server is designed for development and testing.&lt;br&gt;
How do I stop an AI agent's long-running queries? Set a statement_timeout for the role, set it again in your code for every transaction, and make sure the agent can't send SET statements. A pooler or proxy that cancels long queries adds another layer.&lt;br&gt;
Sources and further reading&lt;br&gt;
• PostgreSQL documentation, GRANT: &lt;a href="https://www.postgresql.org/docs/current/sql-grant.html" rel="noopener noreferrer"&gt;https://www.postgresql.org/docs/current/sql-grant.html&lt;/a&gt;&lt;br&gt;
• PostgreSQL documentation, CREATE FUNCTION and SECURITY DEFINER: &lt;a href="https://www.postgresql.org/docs/current/sql-createfunction.html" rel="noopener noreferrer"&gt;https://www.postgresql.org/docs/current/sql-createfunction.html&lt;/a&gt;&lt;br&gt;
• PostgreSQL documentation, client connection defaults: &lt;a href="https://www.postgresql.org/docs/current/runtime-config-client.html" rel="noopener noreferrer"&gt;https://www.postgresql.org/docs/current/runtime-config-client.html&lt;/a&gt;&lt;br&gt;
• PostgreSQL documentation, row security policies: &lt;a href="https://www.postgresql.org/docs/current/ddl-rowsecurity.html" rel="noopener noreferrer"&gt;https://www.postgresql.org/docs/current/ddl-rowsecurity.html&lt;/a&gt;&lt;br&gt;
• PostgreSQL documentation, predefined roles: &lt;a href="https://www.postgresql.org/docs/current/predefined-roles.html" rel="noopener noreferrer"&gt;https://www.postgresql.org/docs/current/predefined-roles.html&lt;/a&gt;&lt;br&gt;
• Supabase documentation, MCP server: &lt;a href="https://supabase.com/docs/guides/getting-started/mcp" rel="noopener noreferrer"&gt;https://supabase.com/docs/guides/getting-started/mcp&lt;/a&gt;&lt;br&gt;
SEO notes (not part of the post)&lt;br&gt;
Why this angle. I searched the main phrase, how to connect an AI agent to a database safely, and the results are crowded: a dev.to post from September 2026 on giving an agent safe, read-only access, a hoop.dev guide, an Airbyte guide on connection patterns, Microsoft's SQL MCP Server documentation and several open-source projects. A generic how-to would compete with all of them. This post targets a narrower phrase with the same intent, read-only Postgres role for AI agents, and brings something those results don't: tested results, with the SQL to reproduce them. That is your best chance to rank and to be linked. I can't give you search volumes, so check the phrase in a keyword tool before you commit.&lt;br&gt;
Keyword placement&lt;br&gt;
Element&lt;br&gt;
What it contains&lt;br&gt;
Title&lt;br&gt;
Read-Only Postgres Role for AI Agents (primary keyword first)&lt;br&gt;
First sentence&lt;br&gt;
The primary keyword, with a clear claim&lt;br&gt;
Description (front matter)&lt;br&gt;
Primary keyword and the numbers 7 and PostgreSQL 16&lt;br&gt;
Quick answer&lt;br&gt;
A direct answer in about 90 words, for featured snippets and answer engines&lt;br&gt;
H2 headings&lt;br&gt;
Each gap, plus FAQ questions in the words people use&lt;br&gt;
Secondary keywords&lt;br&gt;
AI agent database access, Postgres read-only user, default_transaction_read_only, statement_timeout, SECURITY DEFINER, row-level security, Supabase, MCP, prompt injection&lt;br&gt;
Tags&lt;br&gt;
postgres, ai, security, database (dev.to allows four&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fottlzth7hvq4138h2gcv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fottlzth7hvq4138h2gcv.png" alt=" " width="800" height="344"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>ai</category>
      <category>security</category>
      <category>database</category>
    </item>
    <item>
      <title>Your AI Agent Isn't the Problem: Its Database Access Is</title>
      <dc:creator>Claix</dc:creator>
      <pubDate>Sun, 13 Sep 2026 12:42:07 +0000</pubDate>
      <link>https://dev.to/claix/your-ai-agent-isnt-the-problem-your-documents-are-still-unusable-eb5</link>
      <guid>https://dev.to/claix/your-ai-agent-isnt-the-problem-your-documents-are-still-unusable-eb5</guid>
      <description>&lt;p&gt;Most AI agent workflows don't fail because the model is incapable.&lt;br&gt;
They fail because the agent gets the wrong data, or gets the right data in the wrong shape.&lt;br&gt;
A typical business workflow starts with a question like these:&lt;br&gt;
• Has this customer accepted marketing consent?&lt;br&gt;
• What is the status of this order?&lt;br&gt;
• Which reservation does this guest have next?&lt;br&gt;
The answer lives in a relational database such as Postgres or Supabase.&lt;br&gt;
The agent is then expected to:&lt;br&gt;
• Find the right tables&lt;br&gt;
• Understand what the columns and codes mean&lt;br&gt;
• Join tables correctly&lt;br&gt;
• Write a query that is valid and read-only&lt;br&gt;
• Read only what it is allowed to read&lt;br&gt;
• Return a predictable result&lt;br&gt;
• Trigger the next step in an automation&lt;br&gt;
Sometimes it works.&lt;br&gt;
Sometimes the output looks convincing but is wrong: a join that duplicates rows, a status spelled differently so that no rows match, a column nobody should have read, or an answer in a different shape that breaks the next node in the workflow.&lt;br&gt;
This is the part of AI automation that is often underestimated: before an agent can reason about your business data, access to that data needs to be controlled, described and predictable.&lt;br&gt;
The database problem behind many agent failures&lt;br&gt;
When people talk about AI agents, they usually focus on the reasoning layer.&lt;br&gt;
They discuss:&lt;br&gt;
• Which model to use&lt;br&gt;
• How many tools the agent should have&lt;br&gt;
• Whether to use LangGraph, CrewAI, AutoGen, or another framework&lt;br&gt;
• How to create planning loops&lt;br&gt;
• How to give the agent memory&lt;br&gt;
• How to reduce hallucinations&lt;br&gt;
• How to make the agent autonomous&lt;br&gt;
All of those decisions matter.&lt;br&gt;
But in many real-world workflows, the biggest problem appears earlier.&lt;br&gt;
The agent is connected to a database that was never designed to be queried by a language model.&lt;br&gt;
Tables have names like t_ord_2. A status column holds the codes C, P and X. A relationship between two tables exists only in someone's head. Some columns contain internal notes that should never leave the company.&lt;br&gt;
The agent has to solve the data problem and the business problem at the same time.&lt;br&gt;
That creates unnecessary uncertainty.&lt;br&gt;
Imagine an order-status workflow. The actual task may be simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read the customer's question.&lt;/li&gt;
&lt;li&gt;Find the order.&lt;/li&gt;
&lt;li&gt;Read its status.&lt;/li&gt;
&lt;li&gt;Check whether the customer accepted marketing consent.&lt;/li&gt;
&lt;li&gt;Reply, or escalate to a person.
The difficult part is often not the reply.
The difficult part is making sure the agent uses the right table, the right status value, the right join and only the columns it is supposed to read.
Why giving the agent a database connection is fragile
A common first approach is to give a general-purpose model a database connection, or a database MCP server, and let it write SQL.
For example:
Check whether Javier Perez accepted marketing consent and tell me his next reservation.
This can work for prototypes.
However, production workflows usually need stronger guarantees.
The output changes shape. One execution may return:
{
"consent": true,
"next_reservation": "2026-11-07 21:00"
}
Another may return:
Yes, Javier Perez has accepted marketing consent. His next reservation is on Saturday at 9 PM.
Both responses look reasonable to a human. They are not equivalent to an automation.
A downstream node expects a boolean. If it receives a sentence, an object with different key names, or a value that is missing, the workflow can fail silently.
Wrong answers don't raise errors. A query that spells a status value differently runs fine, matches no rows and returns an empty result. Nothing fails, and someone acts on it.
A prompt is not a permission. Instructions hidden in a user message, or in text stored in a database field, can change what an agent does. Supabase's own documentation names prompt injection as the main risk of connecting an agent to your data, and says its MCP server is designed for development and testing. A rule written in a prompt can be overridden. A permission in the database can't.
Context grows. If the agent sees the whole schema, with hundreds of tables, it receives far more than it needs. That increases:
• Token usage
• Latency
• Cost
• The chance of confusing similar tables
• The amount of data the model could reach
There is no record. When an answer looks wrong, you can't see which table the agent used or what query it ran.
The more of your database the model can see, the more important it becomes to control exactly what it can use and what it returns.
A better separation of responsibilities
A more reliable architecture separates data access from agent reasoning.
Instead of asking one general-purpose agent to understand the schema, write the query, protect the data and decide what to do, the workflow can be divided into layers:
Question
↓
Description of your data
↓
Scoped access: only chosen tables and columns
↓
Read-only query
↓
Typed answer
↓
Business rules and agent reasoning
↓
Action or human review
Each layer has a different job.&lt;/li&gt;
&lt;li&gt;Describing your data
This layer says what each table is for and how tables relate to each other.
For example, that orders.customer_id points to customers.id, and that cancelled orders stay in the orders table.&lt;/li&gt;
&lt;li&gt;Scoping access
This layer decides which tables and columns can be used for a given purpose. Whatever is not included doesn't exist for the agent.&lt;/li&gt;
&lt;li&gt;Retrieving
The query itself is a single read-only SELECT, restricted to the scope above. It is the least interesting part, and the part most people try to put first.&lt;/li&gt;
&lt;li&gt;Typing the answer
The result becomes a value the next step can use: a boolean, a number, a text, a date, a list or an object.&lt;/li&gt;
&lt;li&gt;Validating
The workflow checks what comes back before it acts:
• Is the value present?
• Is it the expected type?
• Is it within a reasonable range?
• Does it match what another source says?
• Is the answer empty?&lt;/li&gt;
&lt;li&gt;Reasoning
Only after the data has been retrieved in a usable form should the agent evaluate business logic.
For example:
Does this order exceed the customer's approval limit by more than 5%?
That is a much narrower and more reliable task than asking the same agent to find the tables, write the query and make the decision from scratch.
Introducing Claix
Claix is a managed service that your agent calls to get answers from your database. It is a controlled database retrieval layer for AI agents.
The core idea is simple:
Ask a question. Get a typed answer from the data you allowed.
Claix works with Postgres and Supabase. You set it up in four steps:&lt;/li&gt;
&lt;li&gt;Connect your database with a read-only role.&lt;/li&gt;
&lt;li&gt;Review your Data Map. Claix drafts a description of your tables and the relationships between them, and you correct it.&lt;/li&gt;
&lt;li&gt;Create routes. A route lists the tables and columns that can be used for one purpose, with a short guide of rules.&lt;/li&gt;
&lt;li&gt;Create an agent. It has a name, which becomes its URL, a business prompt, the routes it can use and a response mode.
The read-only role is the foundation. It is a few lines of SQL:
CREATE ROLE claix_agent WITH LOGIN PASSWORD 'use-a-secret-manager'
NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOREPLICATION NOBYPASSRLS;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;GRANT CONNECT ON DATABASE mydb TO claix_agent;&lt;br&gt;
GRANT USAGE ON SCHEMA public TO claix_agent;&lt;br&gt;
GRANT SELECT (id, name, marketing_consent) ON public.customers TO claix_agent;&lt;br&gt;
Columns that aren't granted can't be read, whatever the agent asks.&lt;br&gt;
The business prompt is plain text. For example:&lt;br&gt;
You are the reservations assistant for a restaurant group.&lt;br&gt;
Use the Bookings route for questions about reservations.&lt;br&gt;
Use the Customers route for questions about consent.&lt;br&gt;
Status C means confirmed, P means pending and X means cancelled.&lt;br&gt;
The agent's endpoint becomes /agent/ followed by its name. No code to host, no framework to learn.&lt;br&gt;
A simple API workflow&lt;br&gt;
Claix is designed for backend integrations, scripts, automation platforms and agent tools.&lt;br&gt;
You send one or several questions, each with the type of answer you expect:&lt;br&gt;
const response = await fetch("&lt;a href="https://claix.dev/agent/hr-assistant" rel="noopener noreferrer"&gt;https://claix.dev/agent/hr-assistant&lt;/a&gt;", {&lt;br&gt;
  method: "POST",&lt;br&gt;
  headers: {&lt;br&gt;
    "x-api-key": process.env.CLAIX_API_KEY,&lt;br&gt;
    "Content-Type": "application/json"&lt;br&gt;
  },&lt;br&gt;
  body: JSON.stringify({&lt;br&gt;
    questions: [&lt;br&gt;
      { question: "What is the phone number of the employee named Carlos?", format: "string" },&lt;br&gt;
      { question: "Has the customer Javier Perez accepted marketing consent?", format: "boolean" },&lt;br&gt;
      { question: "What is the job title of Marta?", format: "string" }&lt;br&gt;
    ]&lt;br&gt;
  })&lt;br&gt;
});&lt;/p&gt;

&lt;p&gt;const [result] = await response.json();&lt;/p&gt;

&lt;p&gt;console.log(result.ia_response);&lt;br&gt;
The response keeps each question next to its answer, in the same order:&lt;br&gt;
[&lt;br&gt;
  {&lt;br&gt;
    "user_ask": [&lt;br&gt;
      { "question": "What is the phone number of the employee named Carlos?", "format": "string" },&lt;br&gt;
      { "question": "Has the customer Javier Perez accepted marketing consent?", "format": "boolean" },&lt;br&gt;
      { "question": "What is the job title of Marta?", "format": "string" }&lt;br&gt;
    ],&lt;br&gt;
    "ia_response": [null, true, "Waiter"],&lt;br&gt;
    "log_id": "9ff38974-1392-4c1a-83e5-3b2f09bb43dc"&lt;br&gt;
  }&lt;br&gt;
]&lt;br&gt;
The important part is not just that the agent understood the question.&lt;br&gt;
The important part is that the result is ready to be passed into another system.&lt;br&gt;
For example:&lt;br&gt;
Support ticket arrives&lt;br&gt;
   ↓&lt;br&gt;
Claix agent: order status and consent&lt;br&gt;
   ↓&lt;br&gt;
Branch on the typed values&lt;br&gt;
   ↓&lt;br&gt;
Reply, update the CRM or escalate&lt;br&gt;
The same pattern works with n8n&lt;br&gt;
A common n8n workflow might look like this:&lt;br&gt;
Webhook or Trigger&lt;br&gt;
   ↓&lt;br&gt;
HTTP Request to the Claix agent&lt;br&gt;
   ↓&lt;br&gt;
IF node on a boolean or a number&lt;br&gt;
   ↓&lt;br&gt;
Slack / Email / CRM / Database&lt;br&gt;
The HTTP Request node sends the questions to the agent's URL with your API key. Once the response comes back, the rest of the workflow works with typed values instead of trying to interpret a paragraph or a table of rows.&lt;br&gt;
For a simple approval workflow, the next node might receive:&lt;br&gt;
{&lt;br&gt;
  "order_status": "shipped",&lt;br&gt;
  "order_total": 1284,&lt;br&gt;
  "approval_limit": 1000&lt;br&gt;
}&lt;br&gt;
The agent doesn't need to rediscover the order total.&lt;br&gt;
It can focus on the actual decision:&lt;br&gt;
The order is above the approval limit. Request human approval.&lt;br&gt;
That is a much better use of an agent.&lt;br&gt;
A query result is not the same as an answer&lt;br&gt;
A query returns rows. A business workflow usually needs something more specific:&lt;br&gt;
Has this customer accepted marketing consent?&lt;br&gt;
What is the status of this order?&lt;br&gt;
When is this guest's next reservation?&lt;br&gt;
The rows may contain several columns, several records and values that need interpreting. The workflow needs one value with a meaning.&lt;br&gt;
That is why each Claix agent has a response mode:&lt;br&gt;
Mode&lt;br&gt;
What you get&lt;br&gt;
Good for&lt;br&gt;
Data&lt;br&gt;
The result of the query as it comes out of your database&lt;br&gt;
Code and other agents that process the result themselves&lt;br&gt;
Answer&lt;br&gt;
A reasoned answer in the type you asked for&lt;br&gt;
Results that go straight to a person or to a workflow that needs a single value&lt;br&gt;
In Answer mode, the rows returned by the query are sent to a model so it can write the answer. If your data is sensitive, take that into account when you choose the mode.&lt;br&gt;
Handling missing information safely&lt;br&gt;
One of the most important properties of a data workflow is how it handles missing data.&lt;br&gt;
If the answer is not in the data the agent is allowed to use, the system should not invent a plausible value just to fill the gap.&lt;br&gt;
In the response above, the first answer is null. The phone number is not in the data that agent can use, so Claix doesn't make one up.&lt;br&gt;
A missing value is different from an inferred value.&lt;br&gt;
That distinction matters in finance, legal workflows, compliance, procurement and customer operations.&lt;br&gt;
A downstream workflow can then explicitly decide what to do:&lt;br&gt;
if (result.ia_response[0] === null) {&lt;br&gt;
  return "manual_review";&lt;br&gt;
}&lt;br&gt;
This is more reliable than allowing a model to produce a number that looks right.&lt;br&gt;
Routes and agents: deciding what each agent can see&lt;br&gt;
Not every agent should see the same data.&lt;br&gt;
A support bot, an internal assistant and a workflow check might all work on the same database, and each one needs a different slice of it.&lt;br&gt;
That is the job of routes and agents:&lt;/p&gt;

&lt;p&gt;Route&lt;br&gt;
Agent&lt;br&gt;
Decides&lt;br&gt;
What data can be used, and how to read it&lt;br&gt;
Who asks, and which routes it can use&lt;br&gt;
Contains&lt;br&gt;
Name, description, tables, columns and a guide&lt;br&gt;
Name, description, business prompt, routes and response mode&lt;br&gt;
A route might include only orders.status and orders.total. Another might include only a customer's name and consent. An agent can only use the routes attached to it.&lt;br&gt;
This helps keep the main workflow focused and gives the data layer the responsibility for locating what is relevant.&lt;br&gt;
Access is scoped per agent, not per end user: an agent can read everything its routes include, for every person who calls it. If each person must see only their own rows, enforce that in your database or in your application.&lt;br&gt;
Traces: what happened, and why&lt;br&gt;
Every response includes a log_id.&lt;br&gt;
Use it to look up the request in Traces and see how it was answered: which route was used, the query that ran and what came back.&lt;br&gt;
This matters when an answer looks wrong, because the useful question is not "why did the agent say that?" but one of these:&lt;br&gt;
• Was the question understood?&lt;br&gt;
• Was the right route chosen?&lt;br&gt;
• Was the data missing?&lt;br&gt;
• Was the description of a table unclear?&lt;br&gt;
• Did the business rule calculate incorrectly?&lt;br&gt;
You can also try your agents in the Sandbox with real questions before connecting them to your workflow.&lt;br&gt;
Where this fits in an agent architecture&lt;br&gt;
Claix is not intended to replace the entire agent stack.&lt;br&gt;
It is better understood as a specialist tool, or a data agent.&lt;br&gt;
A general-purpose orchestrator can decide:&lt;br&gt;
This task needs the status of an order. Ask the data agent.&lt;br&gt;
Claix returns a typed value.&lt;br&gt;
The orchestrator then decides what to do next:&lt;br&gt;
The order is shipped. Tell the customer.&lt;br&gt;
This produces a clean division:&lt;br&gt;
Orchestrator:&lt;br&gt;
Planning, routing, decisions, tool selection&lt;/p&gt;

&lt;p&gt;Claix:&lt;br&gt;
Description of your data, scoped access, retrieval, typed answers&lt;/p&gt;

&lt;p&gt;Business system:&lt;br&gt;
CRM, ERP, database, spreadsheet, email, notifications&lt;br&gt;
The main agent does not need to know your schema or write SQL.&lt;br&gt;
It only needs to know when to use the capability and how to consume the result.&lt;br&gt;
Every Claix agent is reachable over REST, MCP and A2A, so the same agent can serve a script, an MCP client or another agent.&lt;br&gt;
A practical example: approving an invoice against the purchase order&lt;br&gt;
Consider a procurement workflow.&lt;br&gt;
The company receives a supplier invoice as a PDF. The purchase order lives in its database.&lt;br&gt;
The workflow needs to determine whether the invoice should be approved.&lt;br&gt;
A fragile implementation might give one agent the PDF and a database connection:&lt;br&gt;
Read this invoice and tell me whether it matches the purchase order.&lt;br&gt;
A more controlled implementation would look like this:&lt;br&gt;
Step 1: Extract the invoice&lt;br&gt;
The invoice arrives as a document, so a document extraction step turns it into fields. Claix Ingest does this, or you can use your own.&lt;br&gt;
{&lt;br&gt;
  "invoice_number": "INV-2026-00456",&lt;br&gt;
  "supplier_name": "Acme Supplies Ltd",&lt;br&gt;
  "total_amount": 1284.5&lt;br&gt;
}&lt;br&gt;
Step 2: Ask the data agent for the purchase order&lt;br&gt;
{&lt;br&gt;
  "questions": [&lt;br&gt;
    { "question": "What is the approved total, in euros, of the purchase order for Acme Supplies Ltd?", "format": "integer" },&lt;br&gt;
    { "question": "Is the supplier Acme Supplies Ltd currently active?", "format": "boolean" }&lt;br&gt;
  ]&lt;br&gt;
}&lt;br&gt;
The answers come back typed:&lt;br&gt;
{&lt;br&gt;
  "ia_response": [1200, true]&lt;br&gt;
}&lt;br&gt;
Step 3: Apply the business rule in code&lt;br&gt;
difference = 1284.5 - 1200 = 84.5&lt;br&gt;
difference_percentage = 84.5 / 1200 = 7.04%&lt;br&gt;
This is arithmetic, so normal code does it.&lt;br&gt;
Step 4: Decide&lt;br&gt;
The invoice is 7.04% above the purchase order. Route to manual review.&lt;br&gt;
The agent is still useful.&lt;br&gt;
It simply is not forced to find the tables, write the query, protect the data and make the decision all at once.&lt;br&gt;
Claix and the A2A direction&lt;br&gt;
As agent systems become more modular, specialized capabilities need to be discoverable and callable by other agents.&lt;br&gt;
A data agent can be useful in an Agent2Agent architecture because it offers a focused capability:&lt;br&gt;
I can answer typed questions about this database, only from the tables I was given.&lt;br&gt;
An orchestrator might delegate a task such as:&lt;br&gt;
Is this customer's marketing consent recorded?&lt;br&gt;
The data agent handles the database-specific work and returns a machine-readable result.&lt;br&gt;
This is preferable to giving every agent direct access to the database and expecting each one to work out the schema.&lt;br&gt;
Specialization can improve:&lt;br&gt;
• Security boundaries&lt;br&gt;
• Permission management&lt;br&gt;
• Cost control&lt;br&gt;
• Observability&lt;br&gt;
• Reusability&lt;br&gt;
• Testing&lt;br&gt;
• Error handling&lt;br&gt;
It also means a data-access capability can be reused across different orchestrators and applications.&lt;br&gt;
Using Claix from Python&lt;br&gt;
A basic request can be made with requests:&lt;br&gt;
import requests&lt;/p&gt;

&lt;p&gt;response = requests.post(&lt;br&gt;
    "&lt;a href="https://claix.dev/agent/hr-assistant" rel="noopener noreferrer"&gt;https://claix.dev/agent/hr-assistant&lt;/a&gt;",&lt;br&gt;
    headers={"x-api-key": "YOUR_API_KEY"},&lt;br&gt;
    json={&lt;br&gt;
        "questions": [&lt;br&gt;
            {"question": "Has the customer Javier Perez accepted marketing consent?", "format": "boolean"},&lt;br&gt;
            {"question": "What is the job title of Marta?", "format": "string"},&lt;br&gt;
        ]&lt;br&gt;
    },&lt;br&gt;
)&lt;br&gt;
response.raise_for_status()&lt;/p&gt;

&lt;p&gt;result = response.json()[0]&lt;/p&gt;

&lt;p&gt;consent, job_title = result["ia_response"]&lt;br&gt;
print(consent, job_title)&lt;br&gt;
print(result["log_id"])&lt;br&gt;
The same pattern can be used from Node.js, n8n, Make, Zapier, an MCP client or a custom backend.&lt;br&gt;
When to use Claix instead of building everything yourself&lt;br&gt;
Building your own data access can be a good choice.&lt;br&gt;
For a small number of fixed questions, a few well-written functions that your agent calls may be all you need.&lt;br&gt;
A managed service becomes more interesting when:&lt;br&gt;
• You have several agents on the same database, each needing different data.&lt;br&gt;
• The questions vary and the rules change.&lt;br&gt;
• You want typed answers rather than text or raw rows.&lt;br&gt;
• You want a record of every request.&lt;br&gt;
• You would rather not host and maintain query code.&lt;br&gt;
• You want to describe your data in plain language and let non-engineers help.&lt;br&gt;
• You want to choose your own model with your own key, or use a managed one.&lt;br&gt;
It is not the right fit when:&lt;br&gt;
• You need per-end-user row filtering inside the tool.&lt;br&gt;
• You use a database other than Postgres or Supabase.&lt;br&gt;
• You need dashboards, charts or governed metrics for analysts.&lt;br&gt;
• You must self-host everything.&lt;br&gt;
The choice depends on your requirements around privacy, latency, cost, control and operational ownership.&lt;br&gt;
Designing reliable database automations&lt;br&gt;
A few practical rules make a significant difference.&lt;br&gt;
Start with a read-only role&lt;br&gt;
Grant SELECT on the columns you want to share, and nothing else. It is the real barrier, and everything else builds on it.&lt;br&gt;
Keep routes focused&lt;br&gt;
Do not include every table if the workflow only needs three.&lt;br&gt;
A smaller route is easier to understand, easier to test and easier to debug.&lt;br&gt;
Separate retrieval from decisions&lt;br&gt;
First get the facts.&lt;br&gt;
Then apply business logic.&lt;br&gt;
This makes it easier to understand whether a failure came from the data or from the rule itself.&lt;br&gt;
Treat missing values explicitly&lt;br&gt;
A missing value should remain missing.&lt;br&gt;
Use null checks or a manual-review branch instead of silently guessing.&lt;br&gt;
Keep the trace&lt;br&gt;
Store the log_id next to important decisions.&lt;br&gt;
This is particularly important for compliance, finance, contracts and healthcare workflows.&lt;br&gt;
Add deterministic checks&lt;br&gt;
Use normal code for things that normal code handles well:&lt;br&gt;
• Date comparisons&lt;br&gt;
• Arithmetic&lt;br&gt;
• Thresholds&lt;br&gt;
• Required fields&lt;br&gt;
• Enum validation&lt;br&gt;
• Duplicate detection&lt;br&gt;
The agent can retrieve the data. It does not need to perform every deterministic operation.&lt;br&gt;
Test with hostile questions&lt;br&gt;
Use the Sandbox with real questions, and with questions that try to read what they shouldn't or to change the rules.&lt;br&gt;
Add a human review path&lt;br&gt;
Not every request should be processed automatically.&lt;br&gt;
Empty answers, conflicting values and ambiguous questions should be routed for review.&lt;br&gt;
A reliable workflow is not one that never asks a human for help.&lt;br&gt;
It is one that knows when it should.&lt;br&gt;
The bigger idea&lt;br&gt;
The future of AI automation is probably not one giant agent that has every tool, sees every table and makes every decision.&lt;br&gt;
A more practical pattern is composable specialization:&lt;br&gt;
Input agent&lt;br&gt;
   ↓&lt;br&gt;
Data retrieval agent&lt;br&gt;
   ↓&lt;br&gt;
Validation layer&lt;br&gt;
   ↓&lt;br&gt;
Reasoning agent&lt;br&gt;
   ↓&lt;br&gt;
Business system&lt;br&gt;
   ↓&lt;br&gt;
Human approval when necessary&lt;br&gt;
Each component has a clear responsibility.&lt;br&gt;
The data layer describes your database, limits what can be read and returns typed answers.&lt;br&gt;
The reasoning layer interprets those answers.&lt;br&gt;
The workflow layer applies rules and triggers actions.&lt;br&gt;
The human remains in control of high-impact decisions.&lt;br&gt;
That architecture is easier to debug than a single autonomous loop because you can inspect each boundary.&lt;br&gt;
When a workflow fails, you can ask:&lt;br&gt;
• Was the question understood?&lt;br&gt;
• Was the right route chosen?&lt;br&gt;
• Was a field missing?&lt;br&gt;
• Did validation reject the result?&lt;br&gt;
• Did the business rule calculate incorrectly?&lt;br&gt;
• Did the agent misunderstand an already-typed value?&lt;br&gt;
That is much more useful than simply knowing that "the agent produced the wrong answer."&lt;br&gt;
Final thoughts&lt;br&gt;
AI models are becoming increasingly capable at reasoning over complex information.&lt;br&gt;
But capability is not the same as reliability.&lt;br&gt;
If your workflow depends on orders, customers, reservations, records or any other data that lives in a database, the quality of your data access will often matter more than the complexity of your agent framework.&lt;br&gt;
Claix is built around a straightforward idea:&lt;br&gt;
Data should stay in your database, and agents should reach only what you allow, through typed answers you can trace.&lt;br&gt;
Once that happens, agents have less irrelevant information to process, workflows can rely on stable values, and developers can use AI for the parts that genuinely require reasoning.&lt;br&gt;
Instead of asking an agent to do everything, give it a controlled way to read your data and a well-defined job.&lt;br&gt;
That is where agent automation starts becoming useful in production.&lt;br&gt;
You can try it without connecting your own database in the Sandbox, or read the docs at claix.dev.&lt;br&gt;
Before you publish (not part of the post)&lt;br&gt;
Item&lt;br&gt;
What to check&lt;br&gt;
Request body&lt;br&gt;
The examples send a list called questions, with a question and a format for each. Your response echoes user_ask, so confirm the real field names&lt;br&gt;
Endpoint&lt;br&gt;
The examples call &lt;a href="https://claix.dev/agent/%7Bname%7D" rel="noopener noreferrer"&gt;https://claix.dev/agent/{name}&lt;/a&gt; with the x-api-key header. Confirm it&lt;br&gt;
Response&lt;br&gt;
The examples use the exact format you shared: a list with user_ask, ia_response and log_id&lt;br&gt;
Types&lt;br&gt;
The examples use string, boolean and integer, which your reader prompt supports. The post says answers can be a boolean, a number, a text, a date, a list or an object. Your reader prompt has no decimal type yet, which is why the purchase order total is an integer&lt;br&gt;
Response modes&lt;br&gt;
The names Data and Answer are mine. Check what your interface calls them&lt;br&gt;
Traces&lt;br&gt;
The post says Traces show the route used, the query that ran and the result. Confirm it&lt;br&gt;
MCP and A2A&lt;br&gt;
The post says every agent is reachable over REST, MCP and A2A. Confirm that is true for each agent&lt;br&gt;
Validation&lt;br&gt;
The post doesn't claim queries are validated before they run. Add it only if it is implemented&lt;br&gt;
Supabase link&lt;br&gt;
The sentence about prompt injection and development use comes from Supabase's MCP documentation: &lt;a href="https://supabase.com/docs/guides/getting-started/mcp" rel="noopener noreferrer"&gt;https://supabase.com/docs/guides/getting-started/mcp&lt;/a&gt;&lt;br&gt;
Claix Ingest&lt;br&gt;
Step 1 of the invoice example mentions it as one option for extracting the invoice&lt;br&gt;
Canonical URL&lt;br&gt;
If you also publish the post on your own blog, add canonical_url to the front matter to avoid duplicate content&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>productivity</category>
      <category>agentskills</category>
    </item>
  </channel>
</rss>
