Manifest reference
The project manifest — every table, field, default, and validation rule, for both kortix.yaml (v2) and kortix.toml (v1, legacy).
The one file the platform treats as authoritative for a project. For the plain-language version, see Projects.
The manifest lives at the repo root — kortix.yaml (v2, the current default for new projects), its kortix.yml short-extension sibling, or kortix.toml (v1, legacy, still fully supported for existing projects). Any repo with a valid manifest at the root is a Kortix project. When more than one candidate file is present, the platform resolves them in priority order — .yaml first, then .yml, then .toml — and reads the first one it finds. The parser is permissive — it never throws on a bad entry: bad triggers and apps go into an errors list alongside the good ones, and unknown top-level keys are ignored (park your own metadata freely).
The canonical schema
Either version, the always-current, machine-checkable spec is the public JSON Schema — generated straight from @kortix/manifest-schema, the same package behind kortix validate and the CR-merge gate, so it can't drift from what the platform actually enforces:
kortix.v2.schema.json—kortix_version: 2only (current default)kortix.v1.schema.json—kortix_version: 1only (legacy)kortix.schema.json— both, dispatched bykortix_version
Point a kortix.yaml's first line at it for editor validation + autocomplete:
# yaml-language-server: $schema=https://kortix.com/schema/kortix.v2.schema.json
kortix_version: 2Or fetch it from the CLI: kortix schema --version 2 (add --url for just the URL).
Full example (v2, kortix.yaml)
# yaml-language-server: $schema=https://kortix.com/schema/kortix.v2.schema.json
kortix_version: 2
default_agent: kortix
project:
name: my-project
description: What this project is.
env:
required: [DATABASE_URL]
optional: [STRIPE_API_KEY, WEBHOOK_SLACK_SECRET]
sandbox:
templates:
- slug: ml
name: ML Development
dockerfile: .kortix/Dockerfile.ml # repo-relative; or `image: python:3.12-slim`
cpu: 4
memory: 16
opencode:
config_dir: .kortix/opencode
agents:
kortix: # governance ONLY — behavior lives in .kortix/opencode/agents/kortix.md
connectors: all
secrets: all
kortix_cli: all
skills: all
release-bot: # a scoped specialist
connectors: [github]
kortix_cli: [project.cr.open] # may open a CR, but not merge it
secrets: [GITHUB_AGENT_TOKEN] # only this secret, not every project secret
triggers:
- slug: daily-digest
name: Daily digest
type: cron
agent: kortix
enabled: true
cron: "0 0 9 * * 1-5" # 09:00 Mon–Fri
timezone: America/Los_Angeles
prompt: |
Summarize yesterday's commits across the repo. Save the result to
notes/digest-{{ fired_at }}.md and open a CR against main.
- slug: slack-hook
name: Slack handler
type: webhook
agent: kortix
enabled: true
secret_env: WEBHOOK_SLACK_SECRET # add value via Secrets Manager
prompt: |
Slack event from {{ headers.user_agent }}.
User said: {{ body.text }}agents: is a governance-only map — see agents: below. There's no [[channels]] in v2 (see Channels).
Legacy example (v1, kortix.toml)
# Pinned schema version. Lets the platform evolve safely.
kortix_version = 1
[project]
name = "my-project"
description = "What this project is."
[env]
required = ["DATABASE_URL"]
optional = ["STRIPE_API_KEY", "WEBHOOK_SLACK_SECRET"]
[[sandbox.templates]]
slug = "ml" # unique per project
name = "ML Development"
dockerfile = ".kortix/Dockerfile.ml" # repo-relative; or `image = "python:3.12-slim"`
cpu = 4
memory = 16
[opencode]
config_dir = ".kortix/opencode"
[[triggers]]
slug = "daily-digest"
name = "Daily digest"
type = "cron"
agent = "kortix"
enabled = true
cron = "0 0 9 * * 1-5" # 09:00 Mon–Fri
timezone = "America/Los_Angeles"
prompt = """
Summarize yesterday's commits across the repo. Save the result to
notes/digest-{{ cron.fired_at }}.md and open a CR against main.
"""
[[triggers]]
slug = "slack-hook"
name = "Slack handler"
type = "webhook"
agent = "kortix"
enabled = true
secret_env = "WEBHOOK_SLACK_SECRET" # add value via Secrets Manager
prompt = """
Slack event from {{ headers.user_agent }}.
User said: {{ body.text }}
"""Schema version
kortix_version: 2kortix_version is the required schema version. A manifest declaring a version higher than the platform knows about is rejected outright — the platform won't silently misread future fields. kortix_version: 2 requires YAML (.yaml); a .toml file declaring kortix_version: 2 is a validation error pointing at the migration flow. When the platform writes the manifest back after a dashboard edit, it ensures kortix_version is the first key.
runtime: (v2 only)
Optional top-level string, v2 only. Today the only legal value is opencode — the default when omitted — reserved so a future runtime (e.g. a hypothetical runtime: claude) is a one-line manifest change rather than a schema break. It has no other effect yet.
runtime: opencodeWhat's parsed where
| Surface | v2 (kortix.yaml) | v1 (kortix.toml, legacy) |
|---|---|---|
| Trigger sweep | triggers: | [[triggers]] |
| Sandbox builder | sandbox.templates | [[sandbox.templates]] |
| Sandbox runtime | opencode: (where to launch opencode with its config) | [opencode] |
| Session bootstrap | env: (advisory — surfaced to dashboard, not enforced) | [env] |
| Connector sync | connectors: | [[connectors]] |
| Agent governance | agents: (governance-only name→block map — server-side launch roster + connector/kortix_cli/secrets/skills grants) | [[agents]] (array of tables, same grant fields plus dead model/file) |
| Apps deploy sweep | apps: (gated per-project — see below) | [[apps]] (same gate) |
| Dashboard UI | All of the above + the raw manifest | Same |
| (removed) | channels: does not exist in the v2 schema — see below | [[channels]] parses and round-trips but no runtime path reads it |
project: / [project]
Optional, human-facing metadata (name, description). The platform does not
currently read this table — a project's display name comes from its own record,
not the manifest — so treat it as a convention for people reading the repo.
env: / [env]
Declares the env var names your sessions need — same key in both versions. Values live in the dashboard's Environment variables page (still project_secrets/secrets under the hood), never inline; the platform decrypts and injects them as plain env vars at session start. (The per-agent grant field is what's renamed in v2 — env → secrets inside each agents: block — see agents: below.)
env:
required: [DATABASE_URL]
optional: [STRIPE_API_KEY]| Field | Type | Notes |
|---|---|---|
required | string[] | Advisory list — surfaced in the dashboard. Not enforced at session start today. |
optional | string[] | Available to sessions if set; absence is fine. |
On enforcement
The dashboard uses required to nag the user about secrets to set, but the session bootstrap does not currently block on missing values. Treat required as a contract with the user, not the platform.
Manifest name validation is permissive — items match ^[A-Z_][A-Z0-9_]*$ (no length cap). The Secrets Manager API caps names at 64 chars (^[A-Z_][A-Z0-9_]{0,63}$), so a longer name is accepted in the manifest but can never get a value. Keep names ≤ 64 chars.
The KORTIX_* prefix is reserved at the Secrets Manager surface, not at parse time: listing KORTIX_FOO in [env] parses fine but can never have a value. Don't. Full contract: Secrets.
sandbox.templates / [[sandbox.templates]]
A list of named, bootable sandbox images; the Kortix runtime layer (opencode CLI, agent daemon, entrypoint) is layered on top of it automatically. Optional — with no entries, every session boots the always-available platform default image. Declare as many templates as you like; a session picks one by slug. This is also where you size the sandbox hardware.
The schema mirrors the runtime provider (Daytona): an image comes either from a Dockerfile in your repo or a public Docker image reference, and the resource spec maps onto the provider's cpu / memory / disk.
sandbox:
templates:
# From a repo Dockerfile
- slug: ml # unique per project; not "default" (reserved)
name: ML Development # optional display label
dockerfile: .kortix/Dockerfile.ml # repo-relative
cpu: 4 # vCPU cores
memory: 16 # GiB
disk: 50 # GiB
# From a public image
- slug: python
name: Python 3.12
image: python:3.12-slim # must be tag- or digest-pinned
cpu: 2
memory: 4v1 (`kortix.toml`, legacy)
[[sandbox.templates]]
slug = "ml"
name = "ML Development"
dockerfile = ".kortix/Dockerfile.ml"
cpu = 4
memory = 16
disk = 50
[[sandbox.templates]]
slug = "python"
name = "Python 3.12"
image = "python:3.12-slim"
cpu = 2
memory = 4| Field | Type | Default | Notes |
|---|---|---|---|
slug | string | — | Required. Unique per project. default is reserved. |
name | string | slug | Display label shown in the dashboard picker. |
dockerfile | string | — | Repo-relative path. Mutually exclusive with image. |
image | string | — | Public Docker image, tag- or digest-pinned (no bare latest). Mutually exclusive with dockerfile. |
entrypoint | string | — | Overrides the container entrypoint the snapshot boots with. Optional — when omitted, the Kortix runtime layer's own entrypoint is used. |
cpu | int | provider default | vCPU cores. |
memory | int | provider default | RAM in GiB. |
disk | int | provider default | Disk in GiB. |
Exactly one of image or dockerfile is required per entry. A Dockerfile path must be repo-relative — absolute paths and .. traversal are rejected. GPUs are not supported in this version. See Sandbox image for what the runtime layer injects and what your Dockerfile can do.
sandbox.default — the project-wide default template
By default every session boots the platform default image; a session opts into a template by passing its slug. Set default on sandbox to make one of your templates the project-wide default instead — then every session (the dashboard's "new session", triggers, channels) boots it without specifying a slug.
sandbox:
default: dev # or "default" for the platform image (the implicit default)
templates:
- slug: dev
dockerfile: .kortix/Dockerfiledefault must name a template defined in this manifest (or the reserved "default"). It's the only key allowed alongside templates — image/build keys directly on sandbox are the removed legacy singular shape and are rejected.
Hardware spec
cpu / memory / disk size the sandbox. Each is independent and optional — leave one out and the runtime provider's default applies. Values are coerced to whole numbers; a value below 1 falls back to the default, and a value above the platform ceiling (cpu 32, memory 128 GiB, disk 500 GiB) is clamped down rather than rejected.
A spec change rebuilds the snapshot
The spec is baked into the project's snapshot at build time — sandboxes inherit their resources from the snapshot, so there's no per-session override. Changing any spec field is part of the snapshot's content hash, so it triggers one rebuild; the new size takes effect on the next session, the same way a Dockerfile edit does. Projects that don't declare a spec are unaffected — their snapshot hash is unchanged.
opencode: / [opencode]
Where OpenCode's config dir lives. Optional, with a default. The agent daemon launches opencode with OPENCODE_CONFIG_DIR pointed here.
opencode:
config_dir: .kortix/opencode| Field | Type | Default | Notes |
|---|---|---|---|
config_dir | string | .kortix/opencode | Repo-relative dir. Same silent-fallback behavior as sandbox.templates paths. |
That directory holds agents, skills, slash commands, tools, plugins, and opencode.jsonc — the layout opencode reads by convention. OpenCode owns the runtime semantics; Kortix may inspect metadata to build server-side agent/model UI surfaces. See Kortix vs OpenCode config and opencode.ai/docs.
opencode.jsonc remains the OpenCode-native registry for plugins, MCP servers,
providers, model/provider settings, permissions, and default runtime behavior.
Do not duplicate those details in the manifest. Use agents: (v2) / [[agents]]
(v1) for the Kortix-side decision of which agents are launchable and what
server-side grants they receive — in v2, EVERY behavioral field (model, mode,
tools, permission, the prompt itself) lives in the agent's own .md frontmatter
instead, never in the manifest.
Legacy v1 projects with no [[agents]] keep OpenCode-native discovery for
backward compatibility. Projects on agents: (v2) or [[agents]] (v1) opt into
declarative, server-side agent discovery: product UI should use the API's
registered agents, while unregistered OpenCode files may still exist for local
experiments or runtime internals. Model pickers should similarly come from the
server / LLM-gateway catalog rather than a sandbox-local OpenCode provider list.
triggers: / [[triggers]]
Each entry fires a session that runs prompt as its initial message — a fresh session by default, or the trigger's prior session when session_mode: reuse (see below). Sorted alphabetically by slug in the parsed output, so UI ordering is stable. Full reference: Triggers.
triggers:
- slug: daily-digest
type: cron
cron: "0 0 9 * * 1-5"
prompt: Summarize yesterday's commits.Common fields
| Field | Required | Type | Default | Notes |
|---|---|---|---|---|
slug | yes | string | — | [a-z0-9][a-z0-9_-]{0,127}, unique among triggers. |
type | yes | string | — | "cron" or "webhook". |
prompt | yes | string | — | Mustache-style template. |
name | no | string | slug | Human label. |
agent | no | string | "default" | Agent name. Legacy projects resolve this through OpenCode discovery; declarative projects should use a registered [[agents]] name. |
enabled | no | bool | true | When false, the scheduler / receiver skip the entry. |
model | no | string | — | Wire form provider/model (e.g. anthropic/claude-sonnet-5). Pins the fired session to that model, taking precedence over the agent/account/platform default chain. Unlike the dead v1 [[agents]].model, this one is live. |
session_mode | no | string | "fresh" | "fresh" or "reuse" (camelCase sessionMode also accepted). "reuse" continues the trigger's existing session on each fire instead of spawning a new one. |
Cron-only: cron (6-field croner second minute hour day month weekday) or a one-off run_at (camelCase runAt, an ISO-8601 datetime, e.g. 2026-06-01T09:00:00Z) — exactly one of the two is required; run_at fires the trigger once at that instant instead of on a recurring schedule. timezone (IANA name, default "UTC") applies to cron. Webhook-only: secret_env (name of a project_secrets entry holding the HMAC secret).
The parser accepts only canonical trigger field names. Slug uniqueness is per-section — a trigger and an app may share a slug; two triggers may not.
Channels — removed in v2
v2 removes channels: from the schema outright. Channel↔agent routing (Slack today; Teams/Discord/etc. later) is live operational state — the dashboard's Channels page or Slack's own slash commands — not declarative config, the same boundary rule that keeps credentials out of git. The channel integration itself still shows up as a connectors entry with provider: channel once you connect one; only the routing table moved out of the manifest.
[[channels]] (v1, legacy)
Array of tables. Each entry documents a chat platform (Slack today) connected to the project. Validated but not wired to any runtime behavior — declaring one, or editing enabled/agent/events/prompt_prefix, changes nothing about how the platform actually routes messages. The live channel→agent binding is the chat_channel_bindings database row, set from inside Slack (its slash commands) or the dashboard's Channels page, not from this file.
[[channels]]
platform = "slack"
enabled = true # optional, default true
agent = "kortix" # optional — parsed, not consumed
events = ["mention", "dm"] # optional, array of strings — parsed, not consumed
prompt_prefix = """
You're working on {{ project.name }}.
{{ message.text }}
"""| Field | Required | Type | Default | Notes |
|---|---|---|---|---|
platform | yes | string | — | e.g. "slack". One entry per platform per project — a duplicate platform is a validation error. |
enabled | no | bool | true | Validated as a boolean. Not read by any runtime path. |
agent | no | string | — | Parsed, not validated, not consumed. |
events | no | string[] | — | Parsed as an array of strings. Not consumed. |
prompt_prefix | no | string | — | Parsed. Not consumed. |
Declared but inert
Connecting Slack from the dashboard writes the install (bot token, signing secret) into the project's secrets and creates the live chat_channel_bindings row — that row, not this manifest block, decides which agent answers in which channel. A hand-written [[channels]] entry documents intent for readers of the repo but has no effect on running behavior. Manage channel routing from Slack or the dashboard's Channels page.
connectors: / [[connectors]]
Each entry connects an external tool the agent can call. The definition lives in git; credentials live in the platform, never here. Full model: Connections.
connectors:
- slug: gmail-work
provider: pipedream # pipedream | mcp | openapi | graphql | http | channel
app: gmail # pipedream app slug
credential: shared # shared is the only mode — see below
policies:
- match: "*"
action: require_approval # always_run | require_approval | block| Field | Required | Type | Notes |
|---|---|---|---|
slug | yes | string | [a-z0-9][a-z0-9_-]{0,127}, unique among connectors; the tool namespace. |
provider | yes | string | pipedream | mcp | openapi | graphql | http | channel. |
name | no | string | Display name. Defaults to slug. |
enabled | no | bool | Defaults to true. |
credential | no | string | shared is the only mode and the default. per_user (per-member BYO credential) was removed 2026-07-05 — a legacy manifest that still says credential: per_user is tolerated and always resolves to shared. |
Provider-specific fields:
- pipedream —
app(required),account(optional, defaults to slug). - mcp —
url(required),transport=http(default) orsse. - openapi —
spec(required: URL or repo-relative path). - graphql —
endpoint(required),spec(optional SDL). - http —
base_url(required),spec(optional). - channel —
platform(required) =slack|email|meet. Chat platforms. You rarely declare these by hand — connecting one (Channels) auto-materializes the connector (credential = the install token, resolved server-side), so the agent can call it through the Executor and you manage it in Connectors. - computer — connected machines over the Agent Computer Tunnel.
Synth-only: you cannot declare it by hand. Connecting a machine
(Computers) auto-materializes a single
computerconnector that fronts all your machines (it has no credential — the live tunnel is the credential; per-machine access is granted in Computers).
Reserved slugs: kortix_slack, kortix_email, kortix_meet (each locked to
provider: channel) and computer (locked to provider: computer) are
platform-owned — declaring one of these slugs with any other provider is a
validation error, the same rule that keeps computer synth-only above.
connectors.auth
Optional. type = bearer | basic | custom | none (default none); in =
header (default) or query; name (required when type = "custom"); prefix
(optional). The credential value is stored in the platform connector credential
store, not in the manifest. Auth is not allowed with provider: pipedream.
connectors.policies / [[connectors.policies]]
Optional per-tool gates. match (glob over tool names, required) + action =
always_run | require_approval | block (required).
agents: (v2)
A name→block map, keyed by agent name — governance only. There is no
model/mode/description/permission/prompt here at all: every behavioral
field lives in that agent's own OpenCode .md file under
.kortix/opencode/agents/<name>.md (frontmatter + body, a stock OpenCode agent
file — no Kortix-specific split). The map key is the join to that .md's
filename. default_agent (top-level, required in v2) must name a declared,
enabled agent here.
default_agent: kortix
agents:
kortix: # the default general-purpose agent
connectors: all # every connector profile
secrets: all # every project secret
kortix_cli: all # full Kortix CLI/API powers (∩ the launching user's role)
skills: all
release-bot: # a scoped specialist
connectors: [github]
kortix_cli: [project.cr.open] # may open a CR, but not merge it
secrets: [GITHUB_AGENT_TOKEN] # only this secret, not every project secret| Field | Required | Type | Default (v2) | Notes |
|---|---|---|---|---|
| (map key) | yes | string | — | [a-z0-9][a-z0-9_-]{0,127}, unique per project. Matches the OpenCode .md filename it governs. |
enabled | no | bool | true | false treats the name as undeclared — default-deny. |
connectors | no | string[] | "all" | "none" | none | Which connector slugs this agent may call. "all" = every connector profile not scoped away from it. |
secrets | no | string[] | "all" | "none" | none | Project secret names this agent receives as sandbox env vars and may read via the secrets API. Renamed from v1's env. |
kortix_cli | no | string[] | "all" | "none" | none | Which Kortix CLI/API actions (open/merge CRs, deploy, manage triggers, spawn sessions, …) this agent may perform. "all" = everything the launching user can do. Effective grant is always userRole ∩ agentGrant — an agent can never exceed its launcher. Run kortix validate --scopes for the full grantable-action list. |
skills | no | string[] | "all" | "none" | none | Which skills this agent may load. Folds onto the compiled OpenCode permission.skill. |
workspace | no | — | — | Reserved for the git-boundary work (workspace/git powers) — not yet enforced. |
v2 is deny-by-default: an omitted connectors/secrets/kortix_cli/skills on a declared agent resolves to none, the opposite of v1's per-field defaults. Give an agent every grant explicitly (as the starter's kortix agent does above) if it should keep full access.
[[agents]] (v1, legacy)
Array of tables. Optional — with no [[agents]] section, every session is capped only by the launching human's role (the project hasn't adopted per-agent governance). Adding any [[agents]] entry opts the whole project into declarative, server-side agent governance: any agent name not listed here is then default-denied — it still runs its OpenCode .md behavior, but with no connectors, no kortix_cli powers, and no project secrets.
An agent's behavior (prompt, mode, tools, permission tree) still lives entirely in its OpenCode .md file under .kortix/opencode/agents/. [[agents]] is a governance overlay on top of that: which agent names the platform will launch, and what each one may touch.
[[agents]]
name = "kortix" # your default general-purpose agent
kortix_cli = "all" # full Kortix CLI/API powers (∩ the launching user's role)
connectors = "all" # every connector profile
[[agents]]
name = "release-bot" # a scoped specialist
connectors = ["github"]
kortix_cli = ["project.cr.open"] # may open a CR, but not merge it
env = ["GITHUB_AGENT_TOKEN"] # only this secret, instead of every project secret| Field | Required | Type | Default | Notes |
|---|---|---|---|---|
name | yes | string | — | [a-z0-9][a-z0-9_-]{0,127}, unique per project. Matches the OpenCode .md filename it governs. |
enabled | no | bool | true | false treats the name as undeclared — default-deny once the project has adopted [[agents]]. |
connectors | no | string[] | "all" | "none" | none ([]) | Which [[connectors]] slugs this agent may call. "all" = every connector profile not scoped away from it. |
kortix_cli | no | string[] | "all" | "none" | none ([]) | Which Kortix CLI/API actions this agent may perform. "all" = everything the launching user can do. Effective grant is always userRole ∩ agentGrant. Run kortix validate --scopes for the full grantable-action list. |
env | no | string[] | "all" | "none" | "all" | Project secret names this agent receives as sandbox env vars and may read via the secrets API. Unlike connectors/kortix_cli, omitting env defaults to all project secrets in v1 — the opposite of v2's deny-by-default. Renamed secrets in v2. |
model | no | string | — | Wire form provider/model (e.g. anthropic/claude-sonnet-5). Dead — parsed and round-tripped, but never applied at runtime; the effective model comes from session/trigger model preferences. Removed outright in v2 (lives in the agent's .md instead). |
file | no | string | conventional .md by name | Override path to the agent's OpenCode behavior file. Dead — parsed but not currently used during materialization. Removed outright in v2. |
The `default` sentinel (v1 only)
In v1, a trigger, channel, or session that names no agent (or names the literal "default") does not resolve to a declared [[agents]] entry — no agent is ever named default. It resolves to OpenCode's own configured default agent (conventionally kortix) and is treated as non-binding: full access, capped only by the launching user's role, exactly as if the project had no [[agents]] at all. v2 removes this ambiguity: default_agent is a required top-level key that must name a declared, enabled agent — there's no unbound sentinel.
Two v1 fields don't do anything
model and file round-trip through a v1 manifest (dashboard edits preserve them) but neither is consulted by the runtime. Don't rely on them to change what a session actually does. Both are removed outright in v2 (that behavior lives in the agent's own .md file).
apps: / [[apps]] (experimental)
Gated per project, not by a single global switch. Apps is one entry in the platform's experimental-feature registry: each project has an explicit choice — on or off — toggled from Customize → Settings, stored in projects.metadata (never in the manifest). KORTIX_APPS_EXPERIMENTAL is only the operator-wide default applied when a project hasn't made an explicit choice; a project can enable Apps even when that env var is false, and can opt out even when it's true. With the effective gate off for a project, its /apps routes return 404 (JSON error pointing at Customize → Settings) and the deploy sweep skips it — entries are parsed but never acted on; the sweep evaluates every active project individually rather than skipping the whole fleet when the operator default is off. Each entry declares a deployable surface alongside the agent, fly.toml-style. The platform dispatches through a provider adapter (Freestyle today; pluggable) and records each deploy in the deployments table.
apps:
- slug: marketing-site
name: Marketing site
enabled: true
framework: next
# domains: [marketing.example.com] # optional — omit for a free *.style.dev URL
source:
type: git # or "tar"
repo: https://github.com/me/site
branch: main
root_path: apps/site
build:
command: pnpm build
out_dir: dist
env:
NEXT_PUBLIC_API_URL: https://api.example.comEntries sort alphabetically by slug; slug uniqueness is per-section.
| Field | Required | Type | Notes |
|---|---|---|---|
slug | yes | string | URL-safe, unique among apps. |
name | no | string | Display name. Defaults to slug. |
enabled | no | bool | Defaults to true. Disabled apps are skipped. |
domains | no | string[] | Optional. Omit it and the platform auto-issues a free *.style.dev URL at deploy time. When present, every entry must be a non-empty string. |
framework | no | string | Hint for the provider adapter (e.g. "next"). |
[apps.source]
| Field | Required for git | Required for tar | Notes |
|---|---|---|---|
type | yes | yes | "git" or "tar". |
repo | no | — | Git clone URL. Falls back to the project's own repo URL if omitted. |
branch | no | — | Defaults to the project's default branch. |
root_path | no | — | Path inside the source to deploy from. Defaults to ".". |
url | — | yes | HTTPS URL of the tarball. |
[apps.build]
| Field | Required | Notes |
|---|---|---|
command | no | Build command. Empty → no build step. |
out_dir | no | Output directory served by the provider. |
If both are empty, the parsed entry collapses to null — the provider treats it as "no build phase."
[apps.env]
Key/value map. Keys must match ^[A-Za-z_][A-Za-z0-9_]*$ (mixed case allowed, unlike [env] secrets which are uppercase-only). Values must be strings — numbers and booleans are rejected.
Hash-based redeploy
Apps redeploy when the manifest-derived hash of their config changes. The hash excludes slug and name, so renaming an app doesn't trigger a redeploy. Edits to source, build, env, domains, framework, or enabled do.
Round-trip rules
Dashboard manifest edits are a read-modify-write on the same file. To keep the diff clean across UI and in-session edits:
- Keep
kortix_versionas the first key. - Inside a trigger entry (
triggers:in v2,[[triggers]]in v1), write fields in this order:slug,name,type,agent,enabled, then type-specific fields, thenpromptlast. - If you add a webhook trigger before its secret is set, declare the secret name in
env.optionalso it shows up in the dashboard's Environment variables page, and leave the triggerenabled: falseuntil the value is in.