TECHNICAL

The Data Access Service, Part 2: Advanced Data Modeling for Agentic Workflows

February 22, 2026 10 min read Part 2 of 3

FireFoundry Team

Engineering

In Part 1, we covered how the Data Access Service gives AI agents governed access to databases through a unified client and semantic dictionary. But access alone is not enough. An agent that can read your tables but doesn't understand what "revenue" means in your business, doesn't know that cancelled orders must be excluded from financial reports, or constructs queries by concatenating SQL strings -- that agent is a liability, not an asset. Part 2 is about giving agents genuine business intelligence: an ontology that maps business concepts to data, process models that encode tribal knowledge as rules, and a structured query engine that eliminates SQL injection by construction.

Code examples are simplified for clarity. See the DAS documentation for complete API reference.

The Data Access Service Series

2 Advanced Data Modeling for Agentic Workflows (You are here)

The Ontology Layer: Business Domains as Data

The ontology is a formal model of your business domain -- not just what tables exist, but what they mean in business terms. It bridges the gap between how humans talk about the business ("show me revenue by segment for premium buyers") and how data is actually stored in the database (SELECT SUM(total_amount) FROM orders JOIN customers USING (customer_id) WHERE customer_segment = 'Premium').

The DAS ontology uses five node types:

Here is how entity resolution works in practice:

// When a user says "premium buyers in California," the agent
// uses the ontology to resolve each term to data constructs.

const context = await dasClient.getOntologyContext({
  connection: 'firekicks',
  domain: 'customer',
});

// context.entityTypes includes Customer with:
//   contextClues: ["buyer", "purchaser", "account", "client"]
//   columns: [
//     { column: "cust_seg_cd", role: "category", businessName: "Customer Segment" },
//     { column: "cust_st", role: "category", businessName: "Customer State" },
//   ]

// The agent resolves "premium buyers" -> Customer entity, cust_seg_cd = 'Premium'
// And "California" -> cust_st = 'CA'

Why this matters: Without an ontology, every agent must be taught individually what your business terms mean. With an ontology, the knowledge lives in the platform. Deploy a new agent and it immediately understands your domain vocabulary.

Business Process Models: Encoding Tribal Knowledge

Raw schemas tell you what data exists. Ontologies tell you what it means. But neither tells you how the business actually works. That tribal knowledge -- the rules everyone follows but nobody documented -- is what process models capture.

Process models have five node types:

Here is a before-and-after example showing how process models improve query correctness:

// WITHOUT process models -- the agent's naive query:
// SELECT SUM(total_amount) FROM orders
//   WHERE order_date BETWEEN '2025-10-01' AND '2025-12-31'
//
// Problems:
// - Includes cancelled orders (hard rule violation)
// - Includes pending orders (soft rule default)
// - Assumes calendar Q4, but company fiscal Q4 might differ

// WITH process models -- the agent consults rules first:
const rules = await dasClient.getBusinessRules({
  connection: 'firekicks',
  table: 'orders',
});

const calendar = await dasClient.getCalendarContext({
  connection: 'firekicks',
  domain: 'finance',
});

// rules includes:
//   { name: "exclude_cancelled_from_revenue",
//     enforcement: "hard_enforced",
//     conditions: [{ column: "order_status", operator: "not_in",
//                    values: ["cancelled"] }] }

// calendar includes:
//   { fiscalYearStartMonth: 1,
//     quarterMapping: { Q4: { startMonth: 10, endMonth: 12 } } }

// The agent now builds the correct query:
// SELECT SUM(total_amount) FROM orders
//   WHERE order_date BETWEEN '2025-10-01' AND '2025-12-31'
//     AND order_status NOT IN ('cancelled')
//     AND order_status IN ('shipped', 'delivered')

The business rules prevented the agent from including cancelled and pending orders. The calendar context confirmed the correct date range. Without process models, the agent would have returned a number that includes refunded orders and shipments still in transit -- a number that no analyst at your company would trust.

AST Queries: A Client-Side Programming Model for Query Construction

DAS will happily accept plain SQL. It parses your SQL into an internal AST (Abstract Syntax Tree), applies transformations -- view expansion, business rule injection, security predicates -- and serializes the result to the target database dialect. You can send SQL and never think about the AST at all.

But the AST API exists because you can do all of this application-side too. And that changes everything for agentic workflows.

When an AI agent generates a raw SQL string, you get a string. You can execute it or not. That is the extent of your control. But when the agent generates an AST -- a structured JSON object representing the query's semantics -- you can programmatically inspect it, validate it, transform it, compose it with other AST fragments, and serialize it back to SQL for human review. The AST is not just a transport format for DAS. It is a client-side programming model for compositional query construction.

The AST Format

An AST query is a JSON object that mirrors SQL structure without being SQL:

{
  "columns": [
    { "expr": { "column": { "table": "p", "column": "category" } } },
    { "expr": { "function": { "name": "sum", "args": [
        { "column": { "table": "oi", "column": "line_total" } }
      ] } }, "alias": "revenue" }
  ],
  "from": { "table": { "table": "products", "alias": "p" } },
  "joins": [{
    "type": "JOIN_INNER",
    "table": { "table": "order_items", "alias": "oi" },
    "on": { "binary": {
      "op": "BINARY_OP_EQ",
      "left": { "column": { "table": "p", "column": "product_id" } },
      "right": { "column": { "table": "oi", "column": "product_id" } }
    }}
  }],
  "where": { "binary": {
    "op": "BINARY_OP_NOT_IN",
    "left": { "column": { "table": "oi", "column": "order_status" } },
    "right": { "list": {
      "items": [{ "literal": { "stringValue": "cancelled" } }]
    }}
  }},
  "groupBy": [{ "column": { "table": "p", "column": "category" } }],
  "orderBy": [{ "expr": { "column": { "alias": "revenue" } }, "desc": true }]
}

When DAS receives this, it validates the structure, checks access control on every referenced table and column, expands any stored view definitions, applies hard-enforced business rules, and serializes to the target dialect. SQL injection is impossible by construction -- every value is a typed literal node, serialized as a bind parameter.

The Full Round-Trip: SQL → AST → Transform → SQL

The real power emerges when you work with the AST client-side. FireFoundry's AST utilities library provides a complete round-trip:

  1. Parse: An LLM generates plain SQL (which LLMs are already good at). Parse it to a typed AST using the client-side parser.
  2. Transform: Apply programmatic transformations -- add WHERE clauses, inject CTEs, enforce business rules -- using composition utilities.
  3. Validate: Run visitor-validators against the AST to verify structural and semantic correctness.
  4. Serialize: Convert back to SQL so the AI (or a human) can review the final query in a readable format.
  5. Accept or iterate: If the review passes, send the AST to DAS for execution. If not, feed the issues back to the LLM and repeat.
// 1. LLM generates SQL
const rawSQL = await llm.generate('Revenue by category for Q4');

// 2. Parse to AST (client-side, no DAS round-trip needed)
const ast = parseSQL(rawSQL);

// 3. Apply programmatic transforms
const withRules = addWhere(ast, and(
  notIn(col('order_status'), [lit('cancelled')]),
  between(col('order_date'), lit('2025-10-01'), lit('2025-12-31')),
));

// 4. Validate
const result = validate(withRules,
  requireDateFilter({ column: 'order_date' }),
  denyColumns(['ssn', 'credit_card']),
);

// 5. Serialize back to SQL for review
const finalSQL = serializeSQL(withRules, { dialect: 'postgresql' });

// 6. Send validated AST to DAS
const data = await dasClient.queryAST('firekicks', { select: withRules });

Multi-LLM Query Assembly

Because the AST is composable data, different LLM calls can produce different parts of a query. This is the canonical pattern for complex analytical queries where no single prompt reliably produces the correct result:

Each LLM call receives a focused, constrained prompt with only the relevant schema. A smaller, more focused prompt produces more accurate output than a single prompt trying to handle date logic, entity resolution, and query structure simultaneously.

The Visitor-Validator Pattern

Here is where it gets interesting. Instead of just validating an AST against static rules, you can have one LLM generate a validator that another LLM's query output must pass.

A visitor-validator walks the AST tree and checks whether specific constraints are satisfied. The pattern works like this:

// LLM A: Analyzes user input, produces a time-period validator
const timeValidator = requireDateFilter({
  column: 'order_date',
  range: { after: '2025-10-01', before: '2025-12-31' },
});

// LLM B: Resolves entities via NER, produces an entity validator
const entityValidator = requireFilter({
  column: 'brand_id',
  values: [42],  // NER resolved "Nike" -> brand_id 42
});

// LLM C: Generates the query AST
const queryAST = await llmC.generateAST(userPrompt, schema);

// Orchestrator: Validate C's output against A's and B's constraints
const result = validate(queryAST, timeValidator, entityValidator);
if (!result.valid) {
  // Feed specific errors back to LLM C for correction
  const corrected = await llmC.fixQuery(queryAST, result.errors);
}

This separation of concerns -- one LLM for temporal reasoning, one for entity resolution, one for query structure -- produces more reliable results than asking a single LLM to handle everything. And because every piece is an inspectable data structure, you can log, audit, and debug every step of the pipeline.

What DAS Does Server-Side

Whether you build the AST client-side or send SQL and let DAS parse it, the server-side processing is the same:

  1. Validates structure -- depth limits, node counts, required fields.
  2. Checks ACL -- walks the AST to find every table and column reference, verifies the caller has access.
  3. Expands views -- stored definitions are transparently injected as subqueries.
  4. Applies business rules -- hard-enforced WHERE clauses from the process model.
  5. Serializes to the target dialect -- PostgreSQL, MySQL, Snowflake, SQL Server, Oracle, SQLite, or Databricks.

The translate-ast endpoint lets you preview the generated SQL without executing -- useful for the review step in the round-trip pattern:

const preview = await dasClient.translateAST({
  connection: 'firekicks',
  select: validatedAST,
});

// preview.sql:      "SELECT p.\"category\", SUM(oi.\"line_total\") AS \"revenue\" ..."
// preview.dialect:  "postgresql"

Putting It All Together

Let us walk through a complete example using all three capabilities. A user asks: "Compare Q4 revenue by product category for premium customers vs. all customers."

Step 1: Resolve business concepts. The agent calls getOntologyContext(domain: 'sales') and learns the Revenue concept definition (SUM(orders.total_amount) WHERE order_status IN ('shipped', 'delivered')), the Customer entity with its segment column, and the Product entity with its category column.

Step 2: Get business rules and calendar. The agent calls getBusinessRules(table: 'orders') and getCalendarContext(domain: 'finance'). It now knows: exclude cancelled orders (hard-enforced), and Q4 = October 1 through December 31.

Step 3: Generate and compose. The agent makes focused LLM calls: one to produce the core query structure (SELECT with aggregation, JOIN to customers), one to produce the date filter from the calendar context, one to produce the segment filter from the ontology resolution. Each call receives only the context it needs. The orchestrator assembles the sub-trees using composition utilities and applies the hard-enforced business rules.

Step 4: Validate. Visitor-validators check that the assembled AST includes a date filter on order_date, excludes cancelled orders, and does not reference unauthorized columns. Any failures produce specific, actionable error messages.

Step 5: Review and execute. The AST is serialized back to SQL. The agent inspects the final query, confirms it matches the user's intent, and sends the validated AST to DAS for execution. DAS applies server-side checks (ACL, view expansion) and runs the query.

Step 6: Return results. The agent returns the comparison table with an explanation of its methodology: which concept definition it used for revenue, which business rules it applied, and what fiscal calendar determined the date range. Every step is logged and auditable.

The ontology tells the agent what to query. The process model tells it what rules to follow. The AST workbench gives it -- and you -- full programmatic control over how the query is built, validated, and reviewed. No black-box SQL generation. No hoping the LLM got it right. Every piece is inspectable, composable, and verifiable.

The Data Access Service Series

2 Advanced Data Modeling for Agentic Workflows (You are here)

FireFoundry Team

Engineering

The FireFoundry team builds enterprise infrastructure for AI agents. We are engineers, product thinkers, and operators who have spent years building and scaling production AI systems. Our mission is to close the gap between AI prototypes and production-grade software.

Related Posts

Request Beta Access

FireFoundry is now in private beta. Join the teams already building production AI agents on the Agent-as-a-Service platform.