Use cases

What you can build from a plan

The cards below are what the tools return for the synthetic shop plan. Back to the use cases.

What you can build automatically from a plan

  1. Connect

    list_workspaces, list_sources

    You sign in with Microsoft and pick a workspace and a lakehouse.

  2. The plan is drawn up

    plan_backend

    You read the keys and relationships; nothing is created.

  3. The plan is checked against the data

    validate_plan

    You approve the plan, or correct a key first.

  4. The parts are created

    create_backend, create_semantic_model, create_ontology, create_views, create_data_agent, create_silver_notebook

    You ask for each part. All but create_backend show what they would create first, and create when you say so.

4 steps. 4 tool calls take you from your tables to a checked plan; then one call per part, 6 parts in all. On the shop schema the plan has 7 tables, 6 with keys and 6 relationships, without anyone writing code or configuration.

From your tables to a checked plan in about 20 minutes; each part after that is one call when you ask for it. The 20 minutes are the budget of the 20-minute demo (connect 4 min, the map 4 min, health check 7 min, the value 5 min), which ends with the plan checked against the rows and the exports in your hands.

Status of creating: create_backend tested end to end; create_semantic_model created live; reviewed in Fabric; create_ontology created live; reviewed in Fabric; create_views draft; deployment not yet verified; create_data_agent draft; deployment not yet verified; create_silver_notebook draft; deployment not yet verified. The runs behind each: test status per tool.

Automatically means: the plan and the cards below come from the engine, and nobody writes code or configuration for them. What is created in Fabric is created when you ask for it. Each card is the output of one tool, run on the synthetic shop plan (the example “A total that doubles” on /check) when this page was built. The status is the tool’s own, from the test status.

A chart along the safest join path

join_path · Tested end to end · docs

Tea141Accessories120Bikes63
Total quantity (order_items.quantity) per categories.name, along order_items → products → categories

Path: order_items → products on product_id = product_id (R1, many-to-one); products → categories on category_id = id (R1, many-to-one); the shortest path is also the safest. Measure SUM('order_items'[quantity]) from semantic_model_draft, summed over the synthetic seed rows.

SQL
SELECT t2.[name] AS label, SUM(t0.[quantity]) AS total
FROM [shop].[order_items] AS t0
JOIN [shop].[products] AS t1 ON t0.[product_id] = t1.[product_id]
JOIN [shop].[categories] AS t2 ON t1.[category_id] = t2.[id]
GROUP BY t2.[name]
ORDER BY total DESC, label

A Power BI semantic model draft

semantic_model_draft · Tested end to end (a draft; creates nothing) · docs

Relationships

  • order_items.order_id → orders.order_id: many-to-one, orders filters order_items
  • order_items.product_id → products.product_id: many-to-one, products filters order_items
  • addresses.customer_id → customers.customer_id: many-to-one, customers filters addresses
  • audit_log.customer_id → customers.customer_id: many-to-one, customers filters audit_log
  • orders.customer_id → customers.customer_id: many-to-one, customers filters orders
  • products.category_id → categories.id: many-to-one, categories filters products
  • orders.order_date → Calendar.Date: many-to-one, Calendar filters orders

Suggested measures

  • orders · Rows in orders: COUNTROWS('orders')
  • order_items · Rows in order_items: COUNTROWS('order_items')
  • order_items · Total quantity: SUM('order_items'[quantity])
  • audit_log · Rows in audit_log: COUNTROWS('audit_log')

Header-level amounts: no header table with an amount column that a line table points at. A number on the header table of a many-to-one relationship is repeated on every line row in a join of the two; summing it over the joined rows counts each header once per line. In the synthetic example order_lines → orders, SUM(orders.order_amount) over the joined lines gives 2000; summed on orders it gives 1000, and the lines add up to 1000.

14 TMDL files; keyless: audit_log.

Rayfin connector entities

rayfin_connector · Tested end to end (a draft; creates nothing) · docs

  • Customers ← customers, key customerId; @many Addresses, @many Orders, @many AuditLog
  • Categories ← categories, key id; @many Products
  • Products ← products, key productId; @one Categories, @many OrderItems
  • Addresses ← addresses, key addressId; @one Customers
  • Orders ← orders, key orderId; @one Customers, @many OrderItems
  • OrderItems ← order_items, key orderId, lineNo; @one Orders, @one Products
  • AuditLog ← audit_log, no key; @one Customers

9 files, including schema.ts.

An app starter prompt and a GraphQL query

app_starter · Tested end to end (a draft; creates nothing) · docs

customers with addresses (OneToMany)

query { customers(first: 10) { items { customer_id name country addresses { items { address_id customer_id city } } } } }
The prompt
Build a small internal web app on top of the GraphQL API "shop_api" (Microsoft Fabric API for GraphQL).

Data model (types, fields and relationships, exactly as the API names them):
- customers (query `customers`, by key, read-only): fields customer_id, name, country; relationships: addresses → addresses (OneToMany), orders → orders (OneToMany), audit_log → audit_log (OneToMany)
- categories (query `categories`, by key, read-only): fields id, name; relationships: products → products (OneToMany)
- products (query `products`, by key, read-only): fields product_id, name, category_id; relationships: category → categories (ManyToOne), order_items → order_items (OneToMany)
- addresses (query `addresses`, by key, read-only): fields address_id, customer_id, city; relationships: customer → customers (ManyToOne)
- orders (query `orders`, by key, read-only): fields order_id, customer_id, order_date, ship_to_id; relationships: customer → customers (ManyToOne), order_items → order_items (OneToMany)
- order_items (query `order_items`, by key, read-only): fields order_id, line_no, product_id, quantity; relationships: order → orders (ManyToOne), product → products (ManyToOne)
- audit_log (query `audit_logs`, read-only): fields event, customer_id, happened_at; relationships: customer → customers (ManyToOne)

Most important views:
- a list/detail page for order_items with its related records expanded
- a search box that filters the list pages; pagination with `first` and `after` on every list query

Access: the API needs a Microsoft Entra bearer token for the scope https://api.fabric.microsoft.com/.default; keep the token exchange server-side and never put a client secret in the browser. Endpoint and example queries below.

Endpoint: https://api.fabric.microsoft.com/v1/workspaces/{workspace-id}/graphqlapis/{api-id}/graphql

Example queries that show the relationships:
```graphql
query { customers(first: 10) { items { customer_id name country addresses { items { address_id customer_id city } } } } }
```
```graphql
query { categories(first: 10) { items { id name products { items { product_id name category_id } } } } }
```
```graphql
query { products(first: 10) { items { product_id name category_id category { id name } } } }
```
```graphql
query { addresses(first: 10) { items { address_id customer_id city customer { customer_id name country } } } }
```
```graphql
query { orders(first: 10) { items { order_id customer_id order_date ship_to_id customer { customer_id name country } } } }
```

Read-only: the API exposes no create/update/delete operations unless the type above says writable.

Backend: create the Supabase edge function `fabric-graphql` with exactly this source and call it from the pages through `src/lib/fabric.ts` below. The secrets ENTRA_TENANT_ID, ENTRA_CLIENT_ID, ENTRA_CLIENT_SECRET, FABRIC_GRAPHQL_ENDPOINT are Supabase secrets; ask me to add them, do not put values in code.

```ts
// supabase/functions/fabric-graphql/index.ts
// Forwards GraphQL queries from the app to Microsoft Fabric. The browser never sees an Entra secret:
// ENTRA_TENANT_ID, ENTRA_CLIENT_ID, ENTRA_CLIENT_SECRET and FABRIC_GRAPHQL_ENDPOINT are Supabase secrets.
// Service principal mode (default): one identity for the whole app (client credentials, scope .default).
// Per-user mode: the browser sends the signed-in user's own Fabric token in the x-fabric-token header
// (Supabase Auth with the Azure provider and the scope https://analysis.windows.net/powerbi/api/GraphQLApi.Execute.All); it is forwarded as is.
const TENANT = Deno.env.get("ENTRA_TENANT_ID") ?? "";
const CLIENT_ID = Deno.env.get("ENTRA_CLIENT_ID") ?? "";
const CLIENT_SECRET = Deno.env.get("ENTRA_CLIENT_SECRET") ?? "";
const ENDPOINT = Deno.env.get("FABRIC_GRAPHQL_ENDPOINT") ?? "https://api.fabric.microsoft.com/v1/workspaces/{workspace-id}/graphqlapis/{api-id}/graphql";
const SCOPE = "https://api.fabric.microsoft.com/.default";
const ALLOWED_ORIGIN = Deno.env.get("APP_ORIGIN") ?? "*";
const CORS = {
  "Access-Control-Allow-Origin": ALLOWED_ORIGIN,
  "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type, x-fabric-token",
};

let cached: { token: string; expires: number } | null = null;

async function appToken(): Promise<string> {
  if (cached && cached.expires > Date.now() + 60_000) return cached.token;
  if (!TENANT || !CLIENT_ID || !CLIENT_SECRET) throw new Error("ENTRA_TENANT_ID, ENTRA_CLIENT_ID and ENTRA_CLIENT_SECRET must be set as secrets");
  const r = await fetch(`https://login.microsoftonline.com/${TENANT}/oauth2/v2.0/token`, {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({ grant_type: "client_credentials", client_id: CLIENT_ID, client_secret: CLIENT_SECRET, scope: SCOPE }),
  });
  if (!r.ok) throw new Error(`token request failed: HTTP ${r.status}`);
  const j = await r.json();
  cached = { token: j.access_token, expires: Date.now() + Number(j.expires_in) * 1000 };
  return cached.token;
}

Deno.serve(async (req) => {
  if (req.method === "OPTIONS") return new Response("ok", { headers: CORS });
  if (req.method !== "POST") return new Response("POST only", { status: 405, headers: CORS });
  let body: { query?: unknown; variables?: unknown };
  try { body = await req.json(); } catch { return new Response(JSON.stringify({ error: "JSON body expected" }), { status: 400, headers: { ...CORS, "Content-Type": "application/json" } }); }
  const query = body.query;
  if (typeof query !== "string" || /^\s*(mutation|subscription)\b/i.test(query)) {
    return new Response(JSON.stringify({ error: "queries only" }), { status: 400, headers: { ...CORS, "Content-Type": "application/json" } });
  }
  const userToken = req.headers.get("x-fabric-token");
  const bearer = userToken && userToken.length > 20 ? userToken : await appToken();
  const r = await fetch(ENDPOINT, {
    method: "POST",
    headers: { Authorization: `Bearer ${bearer}`, "Content-Type": "application/json" },
    body: JSON.stringify({ query, variables: body.variables ?? {} }),
  });
  return new Response(await r.text(), { status: r.status, headers: { ...CORS, "Content-Type": "application/json" } });
});
```

```ts
// src/lib/fabric.ts — the browser talks to the edge function only
import { supabase } from "@/integrations/supabase/client";

export async function fabricQuery<T>(query: string, variables: Record<string, unknown> = {}): Promise<T> {
  const { data, error } = await supabase.functions.invoke("fabric-graphql", { body: { query, variables } });
  if (error) throw error;
  if (data?.errors?.length) throw new Error(data.errors[0].message);
  return data.data as T;
}
```

One of 5 queries app_starter writes. The endpoint has placeholders: the demo has no workspace.