AI Infrastructure

AI Agent Data Infrastructure

One agent can live on a database connection. A fleet of agents needs a shared data layer: live ingestion, a consistent state layer, serving paths for pull, cache and push, memory that feeds back, and access control per agent. This guide lays out that architecture, compares the stacks teams build it from, and sets the whole thing up in SQL.

Last reviewed 2026-11-09. Covers RisingWave 2.6 and later.

Companion guide: Real-Time Context for AI Agents →

5 layers
One Engine
Ingestion, state, retrieval, serving and memory as SQL objects in one streaming database
3 paths
Pull, Cache, Push
Indexed SQL lookups, a Redis mirror for hot keys, and subscription cursors that wake agents on change
1 user
Per Agent
PostgreSQL-style GRANTs limit each agent to its views and its own memory table
Iceberg
Archive
Agent actions and view history land in open tables any lakehouse engine can read

The Problem

Why does existing data infrastructure fail AI agents?

Most companies already have three data systems: an operational database that applications write to, a warehouse that analysts read from, and some event streaming in between. Agents fit none of them well. The warehouse is hours behind and answers in seconds per scan. The operational database is current but holds raw rows, so an agent that needs a balance or a risk score has to aggregate on the production schema, and ten agents doing that is an incident. The event stream is current and cheap to read, but it is a log, not a lookup, and no agent framework reads Kafka directly.

Agents also bring two requirements that analytics never had. They write: every decision, hold, refund or reorder should be recorded where the next decision can see it. And they react: a fraud agent should wake when a pattern appears, not poll a table every ten seconds. Bolting these onto a warehouse means a second database for writes and a scheduler for polling, and the facts the agent reads come from three systems that agree only by coincidence.

The table below lists what agent workloads need against what the three usual systems give them. The right-hand column is what a streaming database provides, because it keeps aggregations current as events arrive, serves them by key, and lets the agent write into the same system it reads from.

RequirementWarehouseOperational databaseStreaming database
FreshnessHours (scheduled loads)Live, but raw and unaggregatedSeconds, computed once per event
Read latencySeconds per scanMilliseconds, if the query is simpleMilliseconds, indexed point lookup
Aggregations and joinsYes, at query timeDangerous on the production databasePrecomputed and maintained
Semantic retrievalSeparate vector storeSeparate vector storeNative vector(n) and HNSW
Write-back of memoryNot designed for itPossible, but mixes agent writes with app dataTables the agent owns, views over them
Consistency across factsPer loadPer query, per tableOne snapshot across all views
Push on changeNoTriggers and LISTEN/NOTIFY, per tableSubscriptions on any view

Reference Architecture

Five layers, one engine

The architecture has five responsibilities. In a conventional stack each is a separate product with its own deployment, schema language and failure modes. In the design on this page every layer is a SQL object in one streaming database: sources for ingestion, materialized views for state and retrieval, indexes, sinks and subscriptions for serving, and ordinary tables for memory. The agents themselves stay where they are, in Python, in an MCP host or in a workflow engine, and talk to the layer over the PostgreSQL protocol.

LayerResponsibilityRisingWave objectConventional equivalent
IngestionBring every fact in as an event: topics for behaviour, CDC for operational state.CREATE SOURCE (Kafka, Pulsar, Kinesis), shared CDC source plus CREATE TABLE ... FROMKafka Connect, Debezium, Fivetran into a warehouse
StateTurn raw events into the facts agents reason over: balances, positions, rolling scores, candidate lists.CREATE MATERIALIZED VIEW, maintained incrementally, consistent at every barrierFlink or Spark jobs writing to a key-value store or warehouse tables
RetrievalMake documents and long text searchable by meaning, kept current as they change.openai_embedding in a view, vector(n) columns, HNSW indexBatch embedding pipeline plus Pinecone, Weaviate or pgvector
ServingDeliver state to agents on their terms: on demand, from a cache, or pushed on change.PostgreSQL protocol plus CREATE INDEX; CREATE SINK to Redis; CREATE SUBSCRIPTION and cursors; MCP serverA REST layer in front of several stores
Memory and archiveKeep what agents decided, feed it back into state, and retain the history.Tables the agent INSERTs into, views over them, CREATE SINK to IcebergApplication database tables plus a nightly export

Two properties come from putting the layers in one engine and are hard to reproduce otherwise. First, consistency: RisingWave advances every view at the same one-second barrier, so an agent that reads account_state, account_risk_1h and account_recent_actions in one query sees the same instant in all three. Second, composability: a derived view such as fraud_candidates is defined over other views, and a subscription on it becomes an event feed for an agent without any extra job.

Two concerns cut across all five layers. Governance is handled with PostgreSQL-style users and GRANTs, so each agent sees only its views and writes only its own memory table. Observability uses the catalog: SHOW JOBS for backfill progress, rw_catalog for state size and parallelism, and Prometheus metrics from every node, including per-function counters for embedding calls.

Reference Implementation

A shared data layer for three agents, in SQL

The scenario is a payments and commerce company running three agents over the same streams. A support agent answers account questions and pulls context per turn. A fraud-review agent reacts to risk patterns and must be woken, not polled. A restock agent reads inventory positions at very high rates and needs cache speed. Payments and inventory events arrive on Kafka; accounts and merchants live in PostgreSQL. Everything below runs on a single-node RisingWave started with the Docker command from the companion guide, or on RisingWave Cloud.

Step 1. Ingest once, for every agent

Topics become append-only sources with watermarks so windows can close. Operational tables arrive through one shared CDC source, which means one replication slot on the production database regardless of how many tables or agents read from it. No agent will ever query these directly.

SQL (RisingWave)
-- Behavioural and transactional events arrive on Kafka as append-only streams.
CREATE SOURCE payments (
  payment_id   BIGINT,
  account_id   BIGINT,
  merchant_id  BIGINT,
  amount       NUMERIC,
  currency     VARCHAR,
  status       VARCHAR,          -- authorized | captured | declined | refunded
  paid_at      TIMESTAMPTZ,
  WATERMARK FOR paid_at AS paid_at - INTERVAL '30 seconds'
) WITH (
  connector = 'kafka',
  topic = 'payments',
  properties.bootstrap.server = 'broker:9092'
) FORMAT PLAIN ENCODE JSON;

CREATE SOURCE inventory_events (
  sku         VARCHAR,
  warehouse   VARCHAR,
  delta       INT,               -- positive for receipts, negative for picks
  on_hand     INT,               -- absolute count after the event
  event_ts    TIMESTAMPTZ,
  WATERMARK FOR event_ts AS event_ts - INTERVAL '10 seconds'
) WITH (
  connector = 'kafka',
  topic = 'inventory',
  properties.bootstrap.server = 'broker:9092'
) FORMAT PLAIN ENCODE JSON;

-- Operational tables come in as change streams through one shared CDC source.
-- Every agent that needs account or merchant state reads these, never the OLTP database.
CREATE SOURCE core_pg WITH (
  connector = 'postgres-cdc',
  hostname = 'pg', port = '5432',
  username = 'rw', password = '...',
  database.name = 'core', schema.name = 'public'
);

CREATE TABLE accounts (
  account_id BIGINT PRIMARY KEY, email VARCHAR, country VARCHAR,
  kyc_level VARCHAR, created_at TIMESTAMPTZ
) FROM core_pg TABLE 'public.accounts';

CREATE TABLE merchants (
  merchant_id BIGINT PRIMARY KEY, name VARCHAR, category VARCHAR, risk_tier VARCHAR
) FROM core_pg TABLE 'public.merchants';

Step 2. Build the shared state layer

Four views cover the three agents. account_state is the entity profile every agent starts from. account_risk_1h is a hopping window that advances every minute. inventory_position keeps the latest count per SKU and warehouse using the group Top-N pattern, which RisingWave recognises and maintains with bounded state. fraud_candidates is a view over views: it encodes the trigger condition in SQL, so the fraud team can change what counts as suspicious without touching agent code.

SQL (RisingWave)
-- Entity state: one row per account, every agent's starting point.
CREATE MATERIALIZED VIEW account_state AS
SELECT a.account_id, a.country, a.kyc_level,
       COUNT(p.payment_id) FILTER (WHERE p.status = 'captured')             AS payments_total,
       COALESCE(SUM(p.amount) FILTER (WHERE p.status = 'captured'), 0)      AS volume_total,
       COUNT(p.payment_id) FILTER (WHERE p.status = 'declined')             AS declines_total,
       MAX(p.paid_at)                                                       AS last_payment_at
FROM accounts a
LEFT JOIN payments p ON p.account_id = a.account_id
GROUP BY a.account_id, a.country, a.kyc_level;

-- Rolling risk signal: declines and volume per account over the last hour, advancing every minute.
CREATE MATERIALIZED VIEW account_risk_1h AS
SELECT account_id, window_start, window_end,
       COUNT(*) FILTER (WHERE status = 'declined')            AS declines_1h,
       COUNT(DISTINCT merchant_id)                            AS merchants_1h,
       COALESCE(SUM(amount) FILTER (WHERE status = 'captured'), 0) AS volume_1h
FROM HOP(payments, paid_at, INTERVAL '1 minute', INTERVAL '1 hour')
GROUP BY account_id, window_start, window_end;

-- Live position: the latest inventory count per SKU and warehouse (group Top-N keeps one row per key).
CREATE MATERIALIZED VIEW inventory_position AS
SELECT sku, warehouse, on_hand, event_ts AS as_of
FROM (
  SELECT *, ROW_NUMBER() OVER (PARTITION BY sku, warehouse ORDER BY event_ts DESC) AS rn
  FROM inventory_events
) t
WHERE rn = 1;

-- Derived candidates: a view other agents and a push subscription can consume.
CREATE MATERIALIZED VIEW fraud_candidates AS
SELECT r.account_id, r.window_end AS flagged_at, r.declines_1h, r.merchants_1h, r.volume_1h,
       s.kyc_level, s.country
FROM account_risk_1h r
JOIN account_state s ON s.account_id = r.account_id
WHERE r.declines_1h >= 3
   OR (r.merchants_1h >= 5 AND s.kyc_level = 'basic');

Step 3. Open three serving paths

The support agent pulls: an index on account_state makes its per-turn read a point lookup, and to_jsonb returns the context ready for the prompt. The restock agent reads from Redis: a sink mirrors inventory_position into the cache and keeps it current, so the view definition remains the single source of truth. The fraud agent is pushed to: a subscription on fraud_candidates retains a day of changes, and the agent holds a cursor on it.

SQL (RisingWave)
-- Path 1: pull. An index turns the account lookup into a millisecond point read.
CREATE INDEX account_state_by_id ON account_state (account_id);

-- The support agent's per-turn read (bind account_id as $1).
SELECT to_jsonb(s) AS account,
       (SELECT to_jsonb(r) FROM account_risk_1h r
         WHERE r.account_id = s.account_id
         ORDER BY r.window_end DESC LIMIT 1)        AS risk_last_hour
FROM account_state s
WHERE s.account_id = $1;

-- Path 2: cache. Mirror a hot view into Redis for an agent that reads thousands of keys per second.
CREATE SINK inventory_cache FROM inventory_position
WITH (
  connector = 'redis',
  primary_key = 'sku,warehouse',
  redis.url = 'redis://cache:6379/'
) FORMAT PLAIN ENCODE JSON (force_append_only = 'true');

-- Path 3: push. A subscription records every change to fraud_candidates for a day.
CREATE SUBSCRIPTION fraud_feed FROM fraud_candidates WITH (retention = '1D');

The push consumer is a loop around FETCH NEXT. The blocking form waits up to the timeout for the next change, so the process idles between events. Each row carries an op column and an rw_timestamp; persisting the timestamp lets the agent resume with a since clause after a restart instead of reprocessing or missing rows.

Python (psycopg 3): the fraud-review agent
import json, psycopg

# The fraud-review agent does not poll. It holds a cursor on the subscription
# and wakes when a row is added to or removed from fraud_candidates.
rw = psycopg.connect("postgresql://fraud_agent@rw:4566/dev", autocommit=True)
cur = rw.cursor()
cur.execute("DECLARE fraud_cur SUBSCRIPTION CURSOR FOR fraud_feed")   # changes from now on

while True:
    cur.execute("FETCH NEXT FROM fraud_cur WITH (timeout = '5s')")   # blocks up to 5 s
    row = cur.fetchone()
    if row is None:
        continue                                                     # nothing changed
    *fields, op, rw_ts = row
    if op == "Insert" or op == "UpdateInsert":
        case = dict(zip(["account_id", "flagged_at", "declines_1h", "merchants_1h",
                         "volume_1h", "kyc_level", "country"], fields))
        decision = review_with_llm(case)                            # your agent loop
        cur.execute(
            "INSERT INTO agent_actions VALUES (%s, 'fraud_review', %s, %s, %s, now())",
            (f"{case['account_id']}-{rw_ts}", case["account_id"],
             decision["action"], json.dumps(decision)))
    persist_checkpoint(rw_ts)   # store rw_timestamp; resume later with 'since <unix_ms>'

Step 4. Record memory and archive it

All three agents write to agent_actions. Two views turn that log into state: the last five actions per account, which the support agent includes in its context, and stale_holds, which checks memory against the live risk view and surfaces holds that should be lifted. An Iceberg sink keeps the full history in open tables for audit and for evaluating agents offline with Spark, Trino or DuckDB.

SQL (RisingWave)
-- Memory: every agent writes what it did to one table. The key includes the agent's name.
CREATE TABLE agent_actions (
  action_id   VARCHAR PRIMARY KEY,
  agent       VARCHAR,
  account_id  BIGINT,
  action      VARCHAR,           -- e.g. 'hold', 'clear', 'refund', 'reorder'
  detail      JSONB,
  acted_at    TIMESTAMPTZ
);

-- Feed memory back into state: the last five actions per account, visible to every agent.
CREATE MATERIALIZED VIEW account_recent_actions AS
SELECT account_id,
       ARRAY_AGG(agent || ':' || action ORDER BY acted_at DESC) AS recent_actions
FROM (
  SELECT *, ROW_NUMBER() OVER (PARTITION BY account_id ORDER BY acted_at DESC) AS rn
  FROM agent_actions
) t
WHERE rn <= 5
GROUP BY account_id;

-- Check memory against reality: holds placed on accounts that have since cleared the risk view.
CREATE MATERIALIZED VIEW stale_holds AS
SELECT a.account_id, a.acted_at AS held_at
FROM agent_actions a
LEFT JOIN fraud_candidates f ON f.account_id = a.account_id
WHERE a.action = 'hold' AND f.account_id IS NULL;

-- Archive: the full action history lands in Iceberg for audit and offline evaluation.
CREATE SINK agent_actions_archive FROM agent_actions
WITH (
  connector = 'iceberg',
  type = 'upsert',
  primary_key = 'action_id',
  warehouse.path = 's3://agent-lake/warehouse',
  database.name = 'agents',
  table.name = 'actions',
  create_table_if_not_exists = 'true',
  commit_checkpoint_interval = '60',
  catalog.type = 'glue',
  catalog.name = 'agent_catalog',
  s3.region = 'us-east-1'
);

Step 5. Govern and observe

Each agent gets a user that can read its views and write only the shared actions table. Sources and CDC tables are not granted to anyone, so raw PII in accounts never reaches a prompt unless a view exposes it deliberately. The same connection strings go into MCP server configurations, one per agent, so an MCP host inherits exactly the same permissions as the Python code would.

SQL (RisingWave)
-- One user per agent. No agent touches a source, a CDC table or another agent's memory.
CREATE USER support_agent WITH PASSWORD '...';
CREATE USER fraud_agent   WITH PASSWORD '...';
CREATE USER restock_agent WITH PASSWORD '...';

GRANT SELECT ON MATERIALIZED VIEW account_state, account_risk_1h, account_recent_actions
  TO support_agent;

GRANT SELECT ON MATERIALIZED VIEW fraud_candidates, account_state, account_recent_actions
  TO fraud_agent;
GRANT INSERT ON TABLE agent_actions TO fraud_agent;

GRANT SELECT ON MATERIALIZED VIEW inventory_position TO restock_agent;
GRANT INSERT ON TABLE agent_actions TO restock_agent;

-- Observe the layer itself.
SHOW JOBS;                                    -- backfill progress of views being created
SELECT * FROM rw_catalog.rw_table_stats;      -- key count and bytes of state per table and view
SELECT * FROM rw_catalog.rw_streaming_parallelism;
MCP client configuration for the support agent
{
  "mcpServers": {
    "risingwave-support": {
      "command": "python",
      "args": ["/opt/risingwave-mcp/src/main.py"],
      "env": {
        "MCP_TRANSPORT": "stdio",
        "RISINGWAVE_CONNECTION_STR": "postgresql://support_agent:...@rw:4566/dev"
      }
    }
  }
}

Every statement above is documented RisingWave SQL: see the subscription, Redis sink, Iceberg sink and GRANT pages. The MCP server is at github.com/risingwavelabs/risingwave-mcp.

Alternatives

How do the common agent data stacks compare?

Each stack below is in production somewhere. The differences that matter for agents are how fresh the state is, how many systems sit between an event and the agent, and whether retrieval, write-back and push are first-class or bolted on.

StackFits whenFreshnessSystems to runGap for agents
Warehouse plus scheduled transforms (Snowflake dynamic tables, BigQuery, dbt)Teams whose agents answer analytical questions and can tolerate minutes of lagMinutes to hoursWarehouse, orchestrator, separate vector storeNo push, no write-back path designed for agents, scan-oriented read latency
OLTP replica plus an API layerA single agent reading a handful of rows from one applicationLiveDatabase, read replica, API service, cacheAggregations and joins run on the production schema; each new fact is a new endpoint
Kafka plus Flink plus Redis plus a vector databaseTeams with a stream processing platform and the engineers to run itSecondsFour, plus the embedding jobNo SQL at read time, no cross-object consistency, every context field is a Java or Python job
Feature platform (Tecton, Databricks Feature Store, Feast)ML teams already serving features to modelsSeconds to minutesFeature platform, online store, separate retrievalFixed-schema numeric features; text retrieval, ad hoc joins and agent memory live elsewhere
PostgreSQL plus pgvector plus cronSmall corpora and low event ratesPer cron runOne, until load forces a splitAggregations are recomputed on a schedule and compete with application traffic
Streaming database (RisingWave, this page)Several agents sharing live operational state, retrieval and memoryAbout one secondOne, plus optional Redis and Iceberg sinksVector indexes need append-only inputs; very large static corpora still favour a dedicated vector store

Capacity and Cost

How do you size it?

The cost model of a streaming data layer is inverted compared with a warehouse. A warehouse charges for queries, so cost grows with the number of agents and turns. A streaming database does the work when events arrive, so cost grows with event rate and view complexity and is nearly flat in the number of agents reading. That is the right shape for agents, whose read volume is unpredictable and whose event volume is not.

ResourceScales withDoes not scale with
Streaming computeEvent rate and view complexity (joins, windows, distinct counts)Number of agents or queries
State storageJoin state and open windows; object storage, so cheap per GBQuery volume
Serving computePoint lookups per second; can run on dedicated serving nodesEvent rate
Cache (Redis sink)Hot keys read thousands of times per secondViews read a few times per minute
Embedding API spendDocument versions publishedAgent questions asked

Three practical rules follow. Keep join state bounded by preferring temporal joins against CDC tables over regular joins between two unbounded streams, and give every window a watermark so its state can be dropped. Put agent reads on dedicated serving compute nodes if the fleet grows, so a burst of agent traffic cannot delay stream processing. And watch rw_catalog.rw_table_stats: the state size of each view tells you which definition to revisit before it tells you in the bill.

Design Decisions

Six questions to settle before you build

One shared cluster for all agents, or one per team?

Start shared. The state layer is more valuable when every agent reads the same account_state and the same fraud_candidates, and consistency across views only holds inside one cluster. Separate by database user and schema, not by cluster. Split only when one team's stream volume or SLA is clearly different from the rest.

When is a Redis mirror worth it?

When a single view is read far more often than it changes, typically thousands of reads per second on a few keys, or when an agent needs sub-millisecond reads inside a tight loop. The sink keeps Redis current from the same view, so the definition lives in one place. Most agent reads are a few lookups per turn and do not need it.

Push or pull?

Pull for conversational agents, whose turns are triggered by a user. Push, through a subscription cursor, for agents whose job is to react: fraud review, restock, escalation, incident response. The condition that defines the trigger is a view, so changing what wakes the agent is a SQL change, not a code change.

How do I change a view that agents depend on?

Create the new definition under a new name, let it backfill (watch SHOW JOBS), point the agent's query or GRANT at it, then drop the old view. Because views are cheap to create and backfill from retained state, running two versions side by side for a day is normal practice, and the agent's user can be granted both during the switch.

Where should embeddings live?

In the same cluster as the structured state, as a view with a vector column and an HNSW index, so a retrieval query can filter on live metadata in the same statement. Keep an external vector database only if it already holds a large corpus you do not want to move; in that case sink changed documents to it from RisingWave rather than running a second pipeline.

How long should agent memory be kept?

Keep the operational view short (the last five actions, the last 90 days) so prompts stay small, and archive everything to Iceberg through a sink. The archive is what you evaluate agents against later: join actions to outcomes offline, without touching the serving cluster.

When Not to Use This

Three cases where a shared streaming layer is premature

  • →A single agent that reads a few rows from one application database. A read replica and a short SQL query are enough; add the state layer when the second agent or the first aggregation arrives.
  • →Agents whose only data is a static document corpus. A vector database or pgvector with a batch embedding job is simpler, and nothing here changes until live operational facts join the prompt.
  • →Workloads that are purely analytical and tolerate hours of lag, such as a weekly report agent. The warehouse you already have is the right place.

Frequently Asked Questions

What is AI agent data infrastructure?
How is it different from a data warehouse or a feature store?
Do I still need a vector database?
How can an agent be notified when something changes, instead of polling?
How do I secure data access for AI agents?
What latency should agents expect?
Can this run on the Kafka and PostgreSQL we already have?
What does it cost to run?

Build the data layer once, for every agent

Run the setup above on RisingWave Cloud or a single Docker container.

Start Free
The engine for real-time streaming data.
Analyze, search, and decide.
GitHubXLinkedInSlackYouTube
Sign up for our to stay updated.