Bootstrap a project

From a real data source to a running app, for a coding agent. Each step ends in something you can check.

0. Prerequisites

  • The MCP server connected with --scope user, and whoami answering (Getting started with the MCP).
  • The companion CLI: make cli in the platform checkout, then cortex login. cortex whoami --json exits 0.
  • Node 22+ in the terminal where the app will live.

The cortex command. It is not on npm, and npx cortex installs an unrelated package with the same name: never run it. make cli leaves ours in ~/.local/bin.

1. Create the project

Agree on the slug (global) with the human.

Ask the human for the region; never choose it for them. region is required and permanent: the lake and the app run there (southamerica-east1 for Brazil, us-central1 for the United States). In dev today the runtime exists in us-central1; a region without a runtime is refused by create_project with an error that names the regions that work. Say so to the human and ask again; never retry in another region on your own.

create_project  { slug: "acme-bi", name: "Acme BI", region: "us-central1" }
get_project     { project_id }     # until status is "active"

2. Scaffold the app

Only after the project is active: cortex.yaml names the project by its slug, and deploy looks it up.

cortex init bi-nextjs --dir acme-bi --slug acme-bi --region us-central1
cd acme-bi && npm install
npm run dev     # http://localhost:3000, sample data, mocked login

cortex init takes the same region, and --region is required: it must be the one the project was created in (get_project shows it).

init writes the Next.js template and its cortex.yaml (spec §8) with your slug, name and region in project:. The template ships sample data shaped exactly like MetricQueryResult, so it runs offline while the project is still empty.

3. Describe the data in cortex.yaml

  • sources: one entry per database, file drop or API. Connection strings are references to secrets (connection: { secret: ERP_DSN }), never values.
  • pipelines: what runs, when, in which timezone.
  • semantic.datasets and semantic.metrics: the only place a number is defined.
  • access.row_policies and access.column_masks: who sees which rows and columns.

Every secret the file names must exist first: set_secret { project_id, name, value }. Then apply the whole file:

apply_project_config  { project_id, yaml: "<contents of cortex.yaml>", dry_run: true }
apply_project_config  { project_id, yaml: "<contents of cortex.yaml>" }

The first apply also syncs the catalog; before it, run_sql answers that the catalog is not synced yet. A protected environment answers with a change set that the human approves (approve_change_set).

4. Load the data

  • Database reachable from the internet: add_source with type: "database", a connector (postgres, mysql, mssql, oracle) and the secret name; test_source; then run_pipeline { project_id, pipeline: "<name under pipelines>" } and get_run { run_id } until it finishes.
  • Spreadsheets and CSVs: upload_file, PUT the bytes to the signed URL, finalize_upload, get_run. For files on the human's disk, cortex upload <files|folder> --json does all four and waits; or leave them for the first deploy (step 6, --with-data).
  • A source inside the human's network (an on-premise ERP) is not reachable from the cloud today, and cortex ingest --local is not implemented yet (it exits 9). Say so.

Verify: list_tables and sample_table show what loaded. Report row counts.

5. Model and query

propose_model { project_id } returns a change set; show it, get it approved, then run_models { project_id }. With the metrics in semantic.metrics applied:

query_metrics  { project_id, query: { metrics: ["revenue", "orders"], dimensions: ["orders.channel"], time_grain: "month" } }

Every number comes with its SQL. Show both.

6. Publish

cortex deploy --json     # production by default; --env preview for a preview

.gitignore and .cortexignore decide what goes in the package; .env* files are always refused. approval.required means a protected environment: approve the change set with the human and run it again. The command waits for the certificate of a new hostname and prints the URL. Check /healthz on it.

First deploy and local data. On the first deploy of a project (no deployment in any environment yet), the human decides whether the data files on their disk go up with the app. Ask them before running the command, or call create_deployment { project_id }: it asks them and answers with the command to run, cortex deploy --with-data or cortex deploy.

  • cortex deploy --with-data --json uploads every local data file first (CSV, TSV, Excel .xlsx, Parquet), waits until each one is loaded, then deploys the app. The files are the type: files sources of cortex.yaml whose path exists on disk; without those, every data file in the folder, .gitignore and .cortexignore applied. Files that went up as data are left out of the app package (--keep-data-in-app ships them too).
  • --data <path|glob>[,...] picks the files (.gitignore does not apply to it); --no-data deploys only the app. --env pr<N> sends the data to that preview.
  • With no flag and no terminal (you, an agent), the command never asks: it deploys only the app and prints a --with-data hint on stderr. --json reports data.decision, data.uploads and data.run_ids.
  • A data failure stops before the app is deployed: exit 7 when an upload is refused, exit 1 with run.failed when a load fails. Read get_run_logs { run_id }, fix the file, run again.
  • .xls is not read: save it as .xlsx.
  • Never uploaded as data, whatever the flag or glob: .env*; keys and credential files (*service-account*.json, *credentials*.json, *.pem, *.key, *.p12, *.pfx, id_rsa*, id_ed25519*, .npmrc, .pypirc); text files whose first 4 KiB hold a private key or a service-account JSON; symbolic links that resolve outside the folder (a link met by a glob or a folder walk is never followed). Each one prints a warning: line on stderr and is listed in refused (data.refused for deploy) with its reason. Named on its own, the command exits 7. Globs skip package.json, tsconfig*.json and other tooling JSON.

Is my app the right size?

Ask the platform, not a guess. get_app_metrics reads CPU, memory, instances, requests, latency p50/p95, 5xx and cold starts of one environment's app, its current size against the plan ceiling, and a recommendation computed from those numbers:

get_app_metrics  { project_id, env: "production", window: "24h" }   # window: 1h, 24h or 7d
recommendation.adviceWhat it meansWhat you do
okThe size fits the loadNothing
scale_upCPU or memory p95 is highPropose recommendation.cortex_yaml
scale_outInstances sit at the maximumPropose recommendation.cortex_yaml
keep_warmCold starts hurt latencyPropose min_instances: 1; it costs while idle
scale_downIt pays for size it never usesPropose the smaller size
upgrade_planThe fix is above the plan ceilingTell the human which plan; do not apply
no_dataNo traffic in the windowOpen the app, or try window: "7d"

The size lives in cortex.yaml, under app.resources (plan-default when absent):

app:
  resources:
    cpu: 2
    memory: 1Gi
    min_instances: 0

Show the human recommendation.reason and monthly_cost_delta_usd (only the number the platform sent; never compute one), then apply_project_config with dry_run: true and again without it. A protected environment answers with a change set to approve. The new size takes effect on the next deploy of that environment. A size above the plan ceiling is refused with limit.plan_required, and its hint names the plan that allows it. The console shows the same numbers on the project page (Machine tab), read-only.

Definition of done

whoami works, cortex.yaml applies, at least one pipeline or upload has run, query_metrics answers with its SQL, and the URL answers on /healthz.

Status today (24/09/2026, dev)

  • Works: cortex login, whoami, init, deploy (with --with-data), upload; every MCP tool in tools/list.
  • Not yet: cortex ingest --local, cortex dev and cortex config push/pull exit with 9 and a hint; use the MCP tools above instead.
  • New on 25/09/2026: get_app_metrics and the required region. If tools/list does not have get_app_metrics yet, this environment was not updated: say so, do not guess the size.
  • The template's dashboard and chat still show sample data. Wiring them to query_metrics is the next change to the template; until then, tell the human the numbers on the page are examples.