KortixDocs
Reference

kortix.toml

The project manifest — every table, field, default, and validation rule.

The one file the platform treats as authoritative for a project. For the plain-language version, see Projects.

kortix.toml lives at the repo root. Any repo with a valid kortix.toml at the root is a Kortix project. 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 tables are ignored (park your own metadata freely).

Full example

# 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 = ["ANTHROPIC_API_KEY", "OPENAI_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 = 1

kortix_version is the schema version. Manifests without it are treated as v1 for backward compatibility (null, 1, and "1" all decode to v1). A manifest declaring a version higher than the platform knows about is rejected outright — the platform won't silently misread future fields. When the platform writes the manifest back after a dashboard edit, it ensures kortix_version is the first key.

What's parsed where

SurfaceWhat it reads
Trigger sweep[[triggers]]
Sandbox builder[[sandbox.templates]]
Sandbox runtime[opencode] (where to launch opencode with its config)
Session bootstrap[env] (advisory — surfaced to dashboard, not enforced)
Connector sync[[connectors]]
Apps deploy sweep[[apps]] (when KORTIX_APPS_EXPERIMENTAL=true)
Dashboard UIAll of the above + the raw manifest

[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 [project] as a convention for people reading the repo.

[env]

Declares the env vars your sessions need. Values live in the Kortix Secrets Manager, never inline; the platform decrypts and injects them as plain env vars at session start.

[env]
required = ["DATABASE_URL"]
optional = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY"]
FieldTypeNotes
requiredstring[]Advisory list — surfaced in the dashboard. Not enforced at session start today.
optionalstring[]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]]

Array of tables. Each entry is one named, bootable sandbox image; 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.

# From a repo Dockerfile
[[sandbox.templates]]
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
[[sandbox.templates]]
slug  = "python"
name  = "Python 3.12"
image = "python:3.12-slim"              # must be tag- or digest-pinned
cpu   = 2
memory = 4
FieldTypeDefaultNotes
slugstringRequired. Unique per project. default is reserved.
namestringslugDisplay label shown in the dashboard picker.
dockerfilestringRepo-relative path. Mutually exclusive with image.
imagestringPublic Docker image, tag- or digest-pinned (no bare latest). Mutually exclusive with dockerfile.
cpuintprovider defaultvCPU cores.
memoryintprovider defaultRAM in GiB.
diskintprovider defaultDisk 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 the [sandbox] table 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.templates]]
slug = "dev"
dockerfile = ".kortix/Dockerfile"

[sandbox]
default = "dev"          # or "default" for the platform image (the implicit default)

default must name a template defined in this manifest (or the reserved "default"). It's the only key allowed on the [sandbox] table itself — image/build keys there 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]

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"
FieldTypeDefaultNotes
config_dirstring.kortix/opencodeRepo-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. Kortix doesn't read those files; opencode does. See Kortix vs OpenCode config and opencode.ai/docs.

[[triggers]]

Array of tables. Each entry spawns a fresh session that runs prompt as its initial message. 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

FieldRequiredTypeDefaultNotes
slugyesstring[a-z0-9][a-z0-9_-]{0,127}, unique among triggers.
typeyesstring"cron" or "webhook".
promptyesstringMustache-style template. Alias: prompt_template.
namenostringslugHuman label.
agentnostring"default"OpenCode agent name. Alias: agent_name.
enablednobooltrueAccepts strings: "true"/"false"/"yes"/"no"/"on"/"off"/"1"/"0".

Cron-only: cron (6-field croner second minute hour day month weekday, alias schedule) and timezone (IANA name, default "UTC"). Webhook-only: secret_env (name of a project_secrets entry holding the HMAC secret, alias secretEnv).

The parser accepts both alias forms (prompt / prompt_template, agent / agent_name, cron / schedule, secret_env / secretEnv). The platform serializes back with the canonical names, so editing-then-saving from the UI normalizes aliases away. Slug uniqueness is per-section — a trigger and an app may share a slug; two triggers may not.

[[connectors]]

Array of tables. 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       = "slack"
provider   = "pipedream"     # pipedream | mcp | openapi | graphql | http
app        = "slack"         # pipedream app slug
credential = "per_user"      # per_user | shared

  [[connectors.policies]]
  match  = "*"
  action = "require_approval"  # always_run | require_approval | block
FieldRequiredTypeNotes
slugyesstring[a-z0-9][a-z0-9_-]{0,127}, unique among connectors; the tool namespace.
provideryesstringpipedream | mcp | openapi | graphql | http.
namenostringDisplay name. Defaults to slug.
enablednoboolDefaults to true.
credentialnostringshared or per_user. Default: per_user for pipedream, shared otherwise.

Provider-specific fields:

  • pipedreamapp (required), account (optional, defaults to slug).
  • mcpurl (required), transport = http (default) or sse.
  • openapispec (required: URL or repo-relative path).
  • graphqlendpoint (required), spec (optional SDL).
  • httpbase_url (required, alias baseUrl), spec (optional).

[connectors.auth]

Optional. type = bearer | basic | custom | none (default none); in = header (default) or query; name (required when type = "custom"); prefix (optional); secret (optional secret name, ^[A-Z_][A-Z0-9_]{0,63}$). Not allowed with provider = "pipedream".

[[connectors.policies]]

Optional per-tool gates. match (glob over tool names, required) + action = always_run | require_approval | block (required).

[[apps]] (experimental)

Gated behind KORTIX_APPS_EXPERIMENTAL=true. With the flag off, /apps routes return 404 (JSON error explaining the flag) and the deploy sweep skips every project — entries are parsed but never acted on. [[apps]] declares deployable surfaces 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"]

  [apps.source]
  type      = "git"            # or "tar"
  repo      = "https://github.com/me/site"
  branch    = "main"
  root_path = "apps/site"

  [apps.build]
  command = "pnpm build"
  out_dir = "dist"

  [apps.env]
  NEXT_PUBLIC_API_URL = "https://api.example.com"

Entries sort alphabetically by slug; slug uniqueness is per-section.

FieldRequiredTypeNotes
slugyesstringURL-safe, unique among apps.
namenostringDisplay name. Defaults to slug.
enablednoboolDefaults to true. Disabled apps are skipped.
domainsnostring[]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.
frameworknostringHint for the provider adapter (e.g. "next").

[apps.source]

FieldRequired for gitRequired for tarNotes
typeyesyes"git" or "tar".
reponoGit clone URL. Falls back to the project's own repo URL if omitted.
branchnoDefaults to the project's default branch.
root_pathnoPath inside the source to deploy from. Defaults to ".".
urlyesHTTPS URL of the tarball.

[apps.build]

FieldRequiredNotes
commandnoBuild command. Empty → no build step.
out_dirnoOutput 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_version as the first key.
  • Inside a [[triggers]] entry, write fields in this order: slug, name, type, agent, enabled, then type-specific fields, then prompt last.
  • If you add a webhook trigger before its secret is set, declare the secret name in [env].optional so it shows up in the Secrets Manager, and leave the trigger enabled = false until the value is in.
kortix.toml | Kortix Docs | Kortix