Skip to content
Concept

Governed Reports and Resource Composition

Decide what an App must publish for reusable reporting, and how Core keeps Reports, Dashboards, and Agent queries under one authorization path.

Governed Reports and Resource Composition

An App does not build a report engine to make its Resource data reusable. It publishes a well-described, actor-authorized Resource. Core then combines only the Resources and relationships it can prove, saves an analysis as a Report, and can place a chosen Report revision on a Dashboard. This keeps an App's business rules and data policy in the App while keeping cross-App planning, revision history, and presentation in Core.

Start with Resource contracts. A Resource must be active, operational for the tenant, safe for the builder, and backed by its normal read permission and row predicate. Its field schema identifies which business fields may be selected, grouped, aggregated, or time-bucketed. Core does not infer any of that from database columns or a list screen.

Choose the smallest App contribution

Most Apps need no reporting-specific code. Keep the Resource descriptor, localized labels, resourceListFields(), and standard authorization accurate; the catalog can use those declarations directly.

Generated Resource modules start with a UUID identity, a reviewed name, and a timestamp baseline. Keep only fields that exist and are safe for reporting; add status enums, references, amounts, or a custom provider only after their labels, capabilities, and row authority have been deliberately reviewed.

Use CompositionQueryContribution only when the App's normal Resource query needs a custom scope, redaction rule, or safe projection that Core cannot express from the standard catalog. Its authorized provider receives the restored actor, request, Resource key, and exact organization target. It returns an already-scoped query plus App-approved aliases and capabilities. It never accepts SQL, a browser column name, or a cross-App model.

ReportingViewDescriptor remains optional App metadata. Publish it through AppDescriptorContribution only when a cataloged surface needs to identify an App-owned reporting view. It is not a saved Report, a permission grant, or a second query API. Do not add it merely because a user could make a Report from your Resource.

NeedApp action
A normal Resource should be selectablePublish safe field capabilities and keep its existing read policy authoritative.
A domain-specific scoped or redacted read is neededImplement CompositionQueryContribution for that Resource.
An App-owned catalog view needs stable metadataOptionally publish ReportingViewDescriptor.
A reusable analysis, chart, export, schedule, or Dashboard placement is neededPublish no new App report contract; Core owns it.

Current composition contract

CompositionSpec has one current schema: schema_version: 1. It names a bounded graph with source aliases and server-issued relationship keys. population is required: an aggregate chooses an anchor, union, or intersection population; a row result also declares its row_source. Every relationship names its source, target, and explicit match behavior. The shape supports typed row and aggregate expressions, nested where predicates, conditional measures, having, comparisons, sort, and bounded limits.

The payload remains semantic intent. A report may request an orders source, an approved ordered_at dimension, and a count measure. It cannot request an orders table, write a join expression, or name a raw SQL aggregate. Core checks the current catalog, proven relationship topology, actor, organization target, field policy, result grain, and query budgets on every preview and execution. Decimal outputs use canonical numeric strings where their declared type proves exact decimal precision.

Amounts and other exact decimal results stay canonical numeric strings from the Composition query through saved Report results, tables, Dashboard metrics, and exports. This includes sum, min, max, comparisons, and derived values. Do not coerce such a value through JavaScript Number; format the string for display and keep the wire value for export. Charts omit a series that cannot be drawn without rounding and identify that limitation instead of changing the reported amount. Ordinary integers remain JSON numbers for compatibility, but an integer beyond JavaScript's safe range is returned as the same canonical numeric string, marked by meta.integer_encoding: canonical_numeric_string_when_unsafe. App providers should publish won amounts as numeric fields and let Core perform the aggregation; they must not pre-round or publish a parallel string-only amount field.

The PHP DTO and matching frontend validator are the syntax authority. A persisted definition uses this one shape; an App publishes no alternate report wire. The catalog includes each authorized Resource's app_key and localized app_label; filtering Apps never widens organization scope.

Report, Dashboard, and Agent have different jobs

A Report is a reusable saved definition with immutable revisions, sharing, and a current revision. Editing or restoring it creates a new revision. A Dashboard owns layout and pins a specific reportRef; a later Report edit does not silently change that placement. Older savedDataWidgetId placements remain copied snapshots for compatibility.

Dashboard common filters are board-local runtime values, never changes to a saved Report or its revision. A board saves at most ten definitions and an explicit mapping for each pinned report: source alias, Resource key, field, and one compatible operator, or an exclusion with its reason. A chart or pivot selection can apply only through that same exact mapping; matching labels does not connect reports. Clearing that selection restores the user's ordinary filter value. Date-only values stay dates; datetime input is local browser time and Core sends its ISO instant consistently to every mapped report.

Sharing a Report lets another actor find and use its definition. It never gives that actor access to a source Resource. Every Report preview, pinned revision query, export, and scheduled run applies the current viewer's report access, organization target, App lifecycle, source permission, and row policy. An unavailable source produces an unavailable or repair state instead of a redacted data leak.

When Core exposes a Report query to an Agent, the Agent uses the same pinned revision and planner as the human surfaces. It has no separate report registry, permission vocabulary, data grant, or App-defined reporting tool. App code continues to publish Resources and their authorization; Core restores the actor and evaluates the same authority for every caller.

A practical decision

Suppose an App exposes an operations.order Resource. Its status, ordered date, and approved amount are safe composition fields, while an internal fraud score is not. The App publishes the safe field capabilities and retains its existing row policy. A user can then build a Report that groups order counts by month, filters to an approved status, adds an allowed conditional measure, and pins a revision to a Dashboard. The App does not create a report table, a Dashboard endpoint, or an Agent tool.

If that App later needs a regional projection that its ordinary Resource query cannot safely express, it adds one authorized composition provider. The provider still applies the App's policy before Core combines the output with other proven sources. That is the extension point; copying Core's planner or loosening a policy to make a chart work is not.

Source of truth: docs/developers/content/en/app-sdk/governed-reports.md