FOCUS Imports
Cloud Ctrl supports importing usage and billing files in the FOCUS format — the FinOps Open Cost and Usage Specification, version 1.0, 1.1 and 1.2 — as an extension of the Custom Usage feature.
Availability
FOCUS imports are enabled per tenant on request. Contact Cloud Ctrl support to have it turned on for your tenant.
Why
Most cloud vendors already publish their billing exports in FOCUS (or are moving to it), and many FinOps tooling pipelines speak the same format. Rather than translating an export into a vendor-specific shape first, pick FOCUS v1.x as the baseline schema when creating the connection and hand Cloud Ctrl the file as your cost system produced it.
FOCUS files ride the exact same import funnel as custom usage pushes: each covered month replaces the data already loaded for that period, rows validate before anything lands, and outcomes appear in the account's Import history.
Picking the baseline schema
When creating a custom usage account (or attaching a custom usage feed to a cloud account), the creation dialog asks for the baseline schema — the field layout your files use:
| Baseline | What it means |
|---|---|
| Cloud Ctrl custom schema | The simplified push schema (default; see Custom Usage) |
| FOCUS v1.x | Canonical FOCUS columns; the Schema tab shows how each maps into Cloud Ctrl |
The baseline is declarative — it drives the sample download and the Schema tab's documentation — and can be changed later on the account's Schema tab.
Accepted files
| Format | Extension | Detection |
|---|---|---|
| FOCUS CSV | .csv | The header contains BilledCost and BillingCurrency |
| FOCUS CSV (gzipped) | .csv.gz | Same, detected after decompression |
| FOCUS Parquet | .parquet | The Parquet schema carries BilledCost and BillingCurrency |
Detection is header-based — configuration doesn't force a format, so a mislabelled file still parses correctly. Files that do not look like FOCUS parse as the simplified custom schema instead.
FOCUS is consumed column-name based and forward compatible: every column is read by its Column ID, so 1.1/1.2 additions (and any future ones) import on 1.0-era code without changes, and columns this import doesn't need are ignored.
A FOCUS file must at minimum carry:
BilledCostEffectiveCostBillingCurrencyChargeCategoryChargePeriodStart
If any of these are missing the file is rejected (fail-closed) with a file-level error in the Import history.
How FOCUS columns map into Cloud Ctrl
| FOCUS column | → in Cloud Ctrl |
|---|---|
ChargePeriodStart | Charge Date (the charge month used for the per-month replace) |
BilledCost | Cost — the cash/invoiced view, the same basis every vendor pipeline carries |
BillingCurrency | Currency (ISO 4217) |
SkuMeter → SkuId → ChargeDescription | Meter Name |
ConsumedQuantity → PricingQuantity | Units Consumed |
ConsumedUnit → PricingUnit | Unit |
ResourceName + ResourceType, else last ResourceId segment, CommitmentDiscountName, ChargeDescription | Service Name (resource-level — see below) |
ResourceId, else SkuId | Service Id |
ServiceSubcategory → ServiceCategory → ChargeDescription | Product Name |
ResourceName → ResourceId → SubAccountName | Resource Name ("All Resources" when none present) |
RegionName → RegionId | Region Name ("Global" when none present) |
Tags (JSON object) | Tags |
SubAccountId → BillingAccountId | Subscription Remote Id |
SubAccountName → BillingAccountName | Subscription Name |
What lands in the Services dimension
Cloud Ctrl's Service grouping is resource-level — it names the thing being metered, exactly as the vendor connections do (Azure keys services by the ARM resource, AWS by the resource inside the ARN). FOCUS rows follow the same rule:
- A row with a resource becomes
friendly-name (resource-type)— a row carryingResourceName: vm-prod-01andResourceType: Microsoft.Compute/virtualMachineslands as the service vm-prod-01 (microsoft.compute/virtualmachines). - Rows with only an id take the last segment of
ResourceId(with the type suffix whenResourceTypeis present). - Commitment purchases (
ChargeCategory: Purchasewith aCommitmentDiscountName) land as the commitment's name — the thing that was bought. - Other aggregate lines (support, credits) take their
ChargeDescription; subscription-level rows with nothing to key on land as All Resources.
The FOCUS ServiceName column — the vendor's service label, e.g. Microsoft.Compute or AWS::EC2 — is not a service in this sense, so it is not used for the Service grouping. It is preserved as focus.service_name metadata on every row; if you want it on a spend surface instead, redirect it with an Override rule on the Schema tab.
The full cost picture rides along
The other FOCUS cost and context columns — EffectiveCost, ListCost, ContractedCost, ChargeCategory, ChargeClass, ChargeFrequency, the CommitmentDiscount* family, Pricing*, SkuId, RegionId, ResourceId, ResourceType, ServiceName (as focus.service_name), invoice fields and more — are preserved on every row as metadata under a focus. prefix (for example focus.effective_cost, focus.commitment_discount_id, focus.resource_type). Your FOCUS extension columns (x_*) pass through to metadata verbatim.
This means:
- Spend lands as billed — the invoiced reality, consistent with how Cloud Ctrl ingests the vendors directly. Accrual views (Effective/List/Contracted) are available for analysis later without re-importing.
- Corrections (
ChargeClass: Correction) land in the calendar month of their charge period, where they are easiest to reason about. - The FOCUS
Tagscolumn is an object; only values are rendered ({"flag": true}becomesflag=true).
Overrides on the Schema tab
The account's Usage feed → Schema tab shows the default mapping above and lets you redirect columns with rules:
- Tag — the column's value becomes a tag under a key you name.
- Override — the column's value replaces a canonical field (for example, use
x_DepartmentNameas the Resource Name). - Metadata (ai.) — the column's value is stored under a metadata key of your choosing.
- Ignore — the column is dropped instead of passing through to metadata.
Unmapped x_* columns still pass through to metadata untouched. Overrides apply to every push of the account — already-imported months are not re-stamped until you push them again.
Sample download
The Push usage tab's sample download reflects the baseline: a FOCUS-baseline account gets a FOCUS v1.x sample CSV (canonical columns with a Usage and a Credit example row that imports cleanly); other accounts get the simplified-schema template. The API serves both: GET /api/ingest/custom-usage/template.csv and GET /api/ingest/custom-usage/template.csv?format=focus.
Validating before you import
The Import history is the source of truth: every file's outcome is stamped per covered month (succeeded, or validation errors) and the errors carry the data row number for quick pinpointing. A failed file ingests nothing for that period until it is corrected and re-run — a corrected re-import replaces the month cleanly.
Verifying the pipeline locally (developer quick check)
One command answers "is the whole FOCUS push-door path working end to end?" — account creation, a FOCUS 1.0 CSV upload, the per-month import, the resource-level Services mapping, and the spend appearing in the UI:
cd CloudCtrl.UI && npx playwright test e2e/focus-imports.spec.ts
Prerequisites:
- The Aspire stack running locally (API on
https://localhost:44351, UI onhttp://localhost:8080) with test authentication enabled. - The test tenant must have Custom Cloud Accounts and Custom Data Sources enabled (CtrlPanel → Tenant Features).
- The Playwright browsers installed (
npx playwright install chromiumonce per machine).
What it asserts, stage by stage:
- The FOCUS-format feed account is created via the API and its baseline schema reads back as
Focus. - A multi-month FOCUS CSV uploads through the CSV door and the queued import succeeds for every billed month — not just any Succeeded state already on the account.
- The imported spend is queryable through the spend API under the resource-level service group (e.g.
octocat (github.copilot/workspaces)), with each billed month carrying its own total. - The account card is visible on the connections screen and the finance dashboard renders with data.
The run deletes the account it created; leftover Focus E2E accounts are from aborted runs and can be removed from the same screen. If the import stage fails, check Seq (http://localhost:5341) for [CustomUsageImport] errors before debugging anything else — the failure is almost always upstream of what the test reports.