Osy#betaRun it on Osyrin: osyrin.com

Reference / Config

warehouse-time budgets (a data-source query is budgeted before it leaves)

admin → org → Budgets: daily data-source time · refused with "data-source time budget"

Every query an application sends to a registered data source is metered and budgeted, in milliseconds of provider time, per person, per application and per organisation. A query that would exceed the day's budget is refused before it is sent, so an unbounded read cannot run up a warehouse bill. Every application has a budget from the moment it exists: leaving the limit blank inherits the platform default, which is a cap and never an opening.

stable2 examples compiled by CIconfigdata-sourcesoperationscost

Summary#

A registered data source is somebody else's database, and somebody else's database bills by the second. So the platform treats a warehouse query exactly as it treats a model call: it is metered, and it is refused before it leaves when the day's budget is spent. Model calls have their own budgets, kept by Osysharp.Llm (Model spend — budgets, the meter and the price book (Llm kit)) — a different unit, and a different ledger.

The budget is milliseconds of provider time: the wall time the platform spent inside your IEntitySource while it answered. That is a proxy for warehouse-seconds and not the warehouse's own bill, because IEntitySource reports neither cost nor elapsed — it is the honest unit the seam admits.

An application always has a budget. A blank limit is not "unlimited": it inherits the platform default. That is the whole guarantee this exists for — nothing bills a warehouse with no cap in front of it, including an app nobody has configured.

Signature#

admin → org → Budgets
   Daily data-source time (ms)   ______   (blank = platform default · 0 = blocked)

Description#

Who is checked, and in what order#

Three scopes, any of which can refuse. The person is checked first, then the application, then its organisation.

ScopeSet whereBlank means
the persona UserSourceBudget row in your app, else the app's per-user defaultthe platform default — a cap, never unlimited
the applicationadmin → org → Budgets, per appthe platform default — a cap, never unlimited
the organisationthe same page, per orgthe organisation caps nothing; only its applications' caps bind

They are not alternatives. An organisation cap bounds what all of its applications may spend between them, so a single app can be inside its own budget and still be refused by its organisation's — and one person can be refused while the application still has most of its day left.

The order is the point. Someone over their own cap costs the application nothing: their query is refused before the application's counter is touched. And a person's reservation is released when a later scope refuses, so a day spent by the application does not also consume every person's day on calls that never left.

What a refused query does#

It fails. It does not return an empty result, and it does not return stale rows — either of those would be a wrong answer presented as a right one. The message names the scope, the limit and the usage:

The data-source time budget for this application is exhausted (18,000,000 ms of 18,000,000 used).
It resets at 00:00 UTC.

The day starts at midnight in the organisation's own time zone when it declares one, not the server's — the same declaration the LLM budget reads, so an application's warehouse day and its token day cannot disagree.

The lint rule that gets ahead of it#

osy lint's cost-unbounded-read catches a query materialized with no Take — the shape that spends a budget fastest, because the cost of a read with no bound is a fact about somebody else's table rather than about your code. It is a SHOULD, not a refusal: an unbounded read is sometimes exactly right (a small reference table you control), and where it is, say so at the site with [SuppressWarning("cost-unbounded-read")] and a reason — see [SuppressWarning]. The budget on this page is the backstop for the ones nobody linted; the rule is how you stop paying for them in the first place.

Why is it counted per source?#

Usage is keyed by the name you gave the source in app.DataSources, not by the entity or the query. An app that reads from two warehouses gets one usage row per warehouse per day, so a bill that surprises you can be attributed to the warehouse that caused it.

using Osysharp.Sources;

class Ledger : IEntitySource {
  public const QueryFeatures Pushdown = QueryFeatures.All;
  public const string SecurityKitVersion = "1";

  public stream<SourceRow> Query(QueryPlan plan) {
    if (plan.Op == "Count") { yield return new SourceRow { Values = ["3"] }; }
  }

  public SourceSchema DescribeSchema() { return new SourceSchema(); }
}

// The NAME each entry is given is the name its spend is recorded under — "Books" and "Analytics",
// never the entity and never the query. An unexpected bill names the warehouse that caused it.
app.DataSources = [ new Ledger("Books"), new Ledger("Analytics") ];

[DataSource(DataSource.Books), ExternalName("SALES.ORDERS")]
entity Order {
  [Key, ExternalName("ORDER_ID")] string Number;
  security { allow read when IsAuthenticated; }
}

// Nothing here opts in. The budget is already in front of this call, because an app has one from
// the moment it exists — a blank limit inherits the platform default rather than meaning unlimited.
int OrderCount() { return Order.Count(); }

How much may one person spend?#

The per-person cap comes from Osysharp.SourceBudget — a capability your app opts into in order to read and override it. Both lines are needed: the use in app.osy is what puts its tables in your database, and the using is what lets this file name UserSourceBudget. Without the use there is no per-person cap in this app at all, and the application and organisation caps are what bound a query.

using Osysharp.SourceBudget;     // and `use Osysharp.SourceBudget;` in app.osy

[Role] enum AppRole { Admin }

[Principal] entity User {
  [Unique, MaxLength(200)] string Email;
  security { allow read when IsAuthenticated; }
}

[RoleGrant] entity RoleGrant {
  [Required] User Grantee;
  [Required] AppRole Level;
  security { allow read when IsAuthenticated; }
}

policy IsAdmin => RoleGrant.Any(g => g.Grantee == user && g.Level == AppRole.Admin);

// The cap for one person, in milliseconds per budget day. 0 blocks them; no row at all means the
// application's per-user default applies, and if that is blank too, the platform's.
partial entity UserSourceBudget {
  security {
    // The capability grants `read` to the person it names and nothing else — nobody raises their own cap.
    // An app whose admins set caps grants the write here, on its own terms.
    allow create, update when IsAdmin;
  }
}

// …and this is what the grant is FOR. Without the partial above, there is no way to write this table
// from Osy# at all.
// ⚠ NO EXECUTION-SIDE ATTRIBUTE HERE, DELIBERATELY. A function that writes an entity is server-side by
// INFERENCE — the platform decides the side, and the reference must never teach the override. Guarded by
// `ExecutionSideIsNotAdvertisedTests.NoPageInTheReference_NamesTheOverride`, which scans the BODY of every
// embedded page, so even naming the attribute in a comment like this one fails it. One was added here on
// 2026-09-26 while making this fence compiled, and the gate caught it.
void SetCap(Guid who, long millis) {
  var cap = new UserSourceBudget { UserId = who, DailySourceMillisLimit = millis };
}

⚑ This example was syntax — illustrative, deliberately not compiled — until 2026-09-26, and it described a program the compiler refused. UserSourceBudget carried the default [Authority(Narrowable)], so a partial could only ever TIGHTEN what the capability granted, and the capability granted read: the allow create, update above was rejected by name. The entity is published [Authority(Extensible)] now — a per-person cap is the app owner's choice, and the capability ships the table precisely so an app can set one — and this fence is COMPILED, so it cannot describe a refused program again without going red.

Three values are consulted, in order, and the first that exists wins:

  1. a UserSourceBudget row for that person, in your app's own data;
  2. the application's per-user default, set beside its own cap in admin → org → Budgets;
  3. the platform default.

Background work has no person, so there is no per-person counter to charge: a workflow step, a scheduled job or a system task is bounded by the application and organisation caps alone. That is a gap in attribution, never in enforcement — no query escapes a cap by having nobody behind it.

⚠ This capability bounds data sources and nothing else. What a model call may spend is Osysharp.Llm's, declared with that kit: a budget filed under the wrong noun makes the manifest — the one place "what may this app reach" is answerable — wrong for everyone reading it afterwards.

Does it hold across regions?#

Yes. On a platform running your application in more than one hosting zone, each zone enforces the organisation's cap against its own spend plus what the other zones have already reported — so the budget is one budget, not one per zone. Near the ceiling the control plane rations what remains between the zones still running. A cap is therefore enforced eventually rather than instantaneously: the overshoot between two syncs is bounded by what remains, which is small precisely when it matters.

The application's cap and its per-person default travel to each zone together, so a cap you raise takes effect on the machines actually running the queries and not only on the page you set it on. A per-person cap is counted in each zone on its own — one person's day is not pooled across regions.

Examples#

admin → org → Budgets → (the app) → Daily data-source time (ms): 0

0 blocks; blank restores the platform default. A blocked app's queries are refused before they leave, and the refusal is counted, so the usage page shows the attempts.

Notes#

  • The unit is provider time, not rows and not bytes. A query that returns one row after a forty-second scan costs forty seconds, which is the number the warehouse charges for and the number you want bounded.
  • A refusal is recorded separately from an AI refusal. A warehouse query refused for warehouse time never touched the token budget, so it never appears as an AI refusal — the two counts are carried apart precisely so the page explaining why the AI stopped answering does not report calls the AI never received.
  • A person's warehouse day and their AI day are separate budgets, deliberately. They are counted in different units against different limits, and someone who has spent their model tokens can still read a warehouse.

See also#

Related

data held somewhere else (app.DataSources)

`app.DataSources` registers the places an app's data can live besides its own database — a warehouse, a SaaS table API…

The Snowflake kit — warehouse tables as entities, and exports back

An optional package that makes Snowflake tables ordinary Osy# entities: your app reads them with LINQ, and each query…

The Databricks kit — lakehouse tables as entities, and exports back

An optional package that makes Databricks tables ordinary Osy# entities: your app reads them with LINQ, and each query…

Model spend — budgets, the meter and the price book (Llm kit)

The Llm kit keeps three things about what model calls cost. The METER: one `ModelCall` row for every attempt, with the…

App configuration

The cross-cutting settings an app declares in one place — the app config object. Two kinds live here: the external…