A factory is defined by a tree of files rooted at factory.yaml, which Kiln validates as a whole and applies whole or not at all.
Layout
factory.yaml
agents/<name>/agent.md # frontmatter + instructions
agents/<name>/skills/<skill>/SKILL.md
automations/<name>/automation.md # triggers + prompt (or automations/<name>.md)
pipelines/<name>.yaml # optional stage graph
gates/<name>.yaml # gate rules
policies/<name>.yaml # protected paths, approvals
decisions/<name>.yaml # typed decisions
runners/<name>.yaml
routers/<name>.yaml
scorers/<name>/scorer.md
benchmarks/<suite>/suite.yaml
benchmarks/<suite>/tasks/<task>.yaml
skills/<skill>/SKILL.md
webhooks/<name>.yaml
memories/<slug>.mdThis layout matches the v1alpha1 factory definition format, with pipelines/, gates/, policies/, decisions/ and memories/ added. Files ending in .md have YAML frontmatter between --- lines and a Markdown body. A file's name (or its directory's name) is the resource's name.
README.md and .gitkeep files are ignored. Any other file Kiln does not recognize, such as .github/workflows/validate.yml, produces an unknown-file warning but does not fail validation.
factory.yaml
schemaVersion: kiln/v1
name: web-app
alias: webapp # @webapp in Slack, Linear, Jira
project: customer-portal
repositories:
- { forge: github, owner: acme, name: web-app }
- { forge: github, owner: acme, name: design-system, deferred: true }
integrations:
- type: slack
slack: { autoRespondToThreadReplies: true }
- linear
github: { factoryConfigChecks: main }
cloudProviders:
aws: { roleArn: arn:aws:iam::123456789012:role/kiln-runs }
agentDefaults:
harness: { type: claude-code, model: router:by-task }
runner: linux-node
selfImprovement: { failedRunThreshold: 15, reviewers: { type: team } }
defaultPipeline: feature # omit to let the foreman choose
budgets: { perWorkItemUsd: 40, monthlyUsd: 6000 }| Key | Notes |
|---|---|
schemaVersion |
Must be kiln/v1. A v1alpha1 tree is converted first; see Importing v1alpha1 definitions. |
name |
1–100 characters. |
alias |
Lowercase letters, digits and dashes, 1–60 characters. Also the factory's ID in Kiln. When missing, the ID is name turned into a slug. |
description, project |
Optional. |
repositories |
At least one. forge is github (default), gitlab, azure_devops or bitbucket. With several repositories, triage picks the one each work item belongs to (from the issue and each repository's optional description) and every stage works in it; with one, agents work in it. deferred: true repositories are not cloned into sandboxes unless an automation's required_repos names them. |
integrations |
The workspace integrations this factory opts into: slack, microsoft-teams, discord, linear, jira, as names or as { type, <type>: settings } objects. Linear and Jira are exclusive. Forges come from repositories. See Connections. |
github |
factoryConfigChecks: main (default) or all. See GitHub. |
automations |
githubOnboardingNotices (default true): once your workspace links GitHub accounts, a person who mentions @kiln-factory without a linked account gets one comment saying how to link it. Work still starts. false turns the comment off for this factory's repositories. |
followUpPermission |
Who can send follow-ups, steer or interrupt this factory's runs once they start: ANYONE_WITH_ACCESS (default), anyone who can send to the run, or RUN_CREATOR, only the person who started the run (or its task's requester). Factory admins are always allowed. Others get HTTP 403 on a run's follow-ups, steer and interrupt; their messages on the task and replies in a chat thread are kept on the task without steering it. Set it in Settings → General info → Who can send follow-ups. |
cloudProviders |
Cloud identity federation for runs. providers is accepted as its older name. See below. |
agentDefaults |
Execution settings every agent inherits. Must set harness or model. |
selfImprovement |
failedRunThreshold 1–50 (default 25), maxAgeDays 1–90 (default 7), reviewers.type admins (default), team, custom or none, and reviewers.emails for custom. |
defaultPipeline |
A file name in pipelines/. |
budgets |
perWorkItemUsd, monthlyUsd. See Policies and budgets. |
Cloud identity federation
cloudProviders:
aws: { roleArn: arn:aws:iam::123456789012:role/kiln-runs }
gcp:
projectNumber: "123456789012" # quote it
workloadIdentityFederationPoolId: kiln
workloadIdentityFederationProviderId: kiln-runs
serviceAccountEmail: runs@acme.iam.gserviceaccount.com # optionalRuns get these environment variables:
| Setting | Variables |
|---|---|
aws.roleArn |
AWS_ROLE_ARN, AWS_ROLE_SESSION_NAME (kiln-run) |
gcp |
GCP_PROJECT_NUMBER, GCP_WORKLOAD_IDENTITY_POOL_ID, GCP_WORKLOAD_IDENTITY_PROVIDER_ID, GCP_WORKLOAD_IDENTITY_PROVIDER (the full projects/…/providers/… name), GCP_SERVICE_ACCOUNT_EMAIL |
Set cloudProviders or the older providers, not both.
The other file kinds are covered on their own pages: agents, skills, automations, pipelines and gates, policies, decisions, runners, scorers and benchmarks. The full key reference is generated from the schema.
Validation
Kiln validates a tree in three steps: parse each file, check it against its schema, then check references across files. Unknown keys are errors.
Cross-file rules:
- Exactly one agent has
agentType: FOREMAN. agentDefaultssetsharnessormodel. No file sets both.- Every
runnernames a file inrunners/, and everyrouter:<name>model names a file inrouters/. defaultPipelinenames a file inpipelines/.spawnableBy, automationagent, scoreragentsand benchmarkagentname agents that exist.webhooktriggers havefilter.webhooknaming a file inwebhooks/.- Pipeline stage IDs are unique,
agentandgateexist, andon_failnames an earlier stage. - Scorer label values and benchmark configuration names are unique.
- Every task a suite lists has a file in its
tasks/folder. skip_if, gateruleand decisionpolicyexpressions parse, and policies only read their own questions and state fields.customself-improvement reviewers have at least one email.
Errors carry the file and line:
agents/triage/agent.md:3: runner missing is not defined in runners/
pipelines/feature.yaml:4: stage spec uses unknown gate spec-approvlRun kiln validate [path] locally or in CI. It exits non-zero on any error. See the CLI reference.
Applying a definition
Kiln stores each tree by its definition SHA, a 40-character hash of every path and file content. Applying a tree:
- Valid tree. Kiln stores it, makes it the applied definition, keeps the previous SHA, and records who applied it and the diff in the audit log. New work items use it. Work items already in flight keep the SHA they started with.
- Invalid tree. Kiln records the attempt and the errors, and the previous definition stays active. The factory shows the first error as its last error.
kiln diff --against applied asks the server what would change compared with the applied definition, by resource:
+ agent/docs
~ agent/review
- automation/weeklykiln apply --dry-run validates and shows the diff without applying.
Canary apply
kiln apply --canary 10% applies a valid tree as a candidate for a share of new work items. The applied definition stays in place for the rest.
- Each new work item is assigned by a stable hash of its ID, so the split stays the same for that item.
- Metrics and scores are split by definition SHA, so you can compare the two.
- You can promote the candidate to 100% or roll it back at any time.
- Kiln checks canaries on its timer. After 7 days, it promotes the candidate if its pass rate is no more than 2 points below the applied definition's, and rolls it back otherwise, including when the candidate has no scores.
A canary only starts when the factory already has an applied definition and the share is below 100%. See Replay and canary.
Config repos on GitHub
A factory can live in a GitHub repository, called its config repo. Use the layout above: factory.yaml at the repository root (or in a subdirectory), agents/<name>/agent.md, and so on. For example, acme/web-factory-config.
Connect. Connecting a config repo reads the tree at its branch (default main) and directory (default the root), applies it, and binds the factory to the repository. If factory.yaml says schemaVersion: v1alpha1, Kiln converts it first; see Importing v1alpha1 definitions.
Sync on push. When Kiln receives a GitHub push webhook for the config repo's branch, it reads the tree again and applies it. A push that breaks validation is recorded and rejected; the last valid definition stays active. You can also trigger a sync by hand.
One exception: when the pushed tree is exactly the candidate of an open self-improvement proposal, which is what merging that proposal's pull request produces, Kiln applies it as a canary (10% by default) and marks the proposal merged. See Self-improvement.
Changes from Kiln. When Kiln itself proposes a definition change, such as a self-improvement, an accepted memory, an installed skill or a router from benchmarks, it commits to a kiln/* branch of the config repo and opens a pull request. A person merges it, and the push applies it.
Protect the config repo's default branch so that every change needs a review and a passing validation check.
Validate pull requests with GitHub Actions
This workflow checks out the config repo inside a Kiln checkout, installs Kiln, and runs pnpm kiln validate . from the config repo's directory. The CLI resolves paths against the directory you ran pnpm from, so . is the config repo.
# .github/workflows/validate.yml
name: Validate factory definition
on:
pull_request:
push:
branches: [main]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- name: Check out Kiln
uses: actions/checkout@v4
with:
repository: fishuke/kiln
path: kiln
- name: Check out the definition
uses: actions/checkout@v4
with:
path: kiln/definition
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
cache-dependency-path: kiln/pnpm-lock.yaml
- name: Install Kiln
working-directory: kiln
run: pnpm install --frozen-lockfile
- name: Validate
working-directory: kiln/definition
run: pnpm kiln validate .The command prints ok <name> (<sha>): <n> agents, <n> automations on success, prints each error as file:line: message and exits 1 on failure. Make the validate job a required status check in the branch protection rules. The workflow file itself is outside the definition layout, so validation lists it as an unknown-file warning.
Editing through the API
The web app edits definitions file by file:
GET /api/v1/factories/{id}/filesandGET|PUT|DELETE /api/v1/factories/{id}/files/{path}.POST /api/v1/factories/{id}/files:batch {changes: [{path, content | null}], message}changes several files in one commit and returns{applied: sha}or{pullRequest: url}.- Higher-level endpoints for agents, automations, runners, routers, skills and settings (
PATCH /factories/{id}/settings: name, alias, description, repositories, credential strategy, agent and scorer defaults, self-improvement, benchmarking model).
A Kiln-managed factory validates the whole tree and applies it atomically; nothing changes on an error. A factory stored in a config repo gets a branch and a pull request instead, and the merge applies it. POST /factories/{id}/migrate {owner, name} moves a managed factory into a GitHub repo through a pull request; the factory switches over when it merges.
Creating a factory
POST /api/v1/factories (and the MCP tool create_factory) is the setup wizard in one call: repositories, which of triage, spec, implement and review to include (the foreman is always there), Slack, a tracker (Linear or Jira, with teams or projects), a cost preset (quality uses Claude Fable 5.1 for the foreman and spec agents and Opus 5.5 to implement; efficient uses Sonnet and Haiku), self-improvement on or off, and starter automations. It writes default prompts for every agent, the github, slack, linear/jira, code-review and ui-verification skills, the default scorers and the automations for the connected tools. dryRun: true returns the files without creating anything. POST /factories/{id}/learning then starts a run that studies the repositories and tools and proposes memories and prompt changes for review.
Schemas and validation for editors and CI
These need no token:
GET /api/v1/factory-files/schemas,/schemas/v1(every document) and/schemas/v1/{document}(for exampleagent.schema.json), withETagandIf-None-Match.POST /api/v1/factory-files/validate {files: [{path, content}]}returnsdiagnostics(path, line, code, message, severity) anddeferredResolutions: models, secrets, MCP servers, worker hosts and aliases that are only checked when the tree is applied. Limits: 256 files, 256 KB per file, 2 MB in total.kiln files validate [path]calls it.
Agent foreman
foreman: { mode: agent } in factory.yaml makes the foreman an agent instead of a fixed list of stages. It works in turns. Each turn it reads the task (the request, the conversation, its checklist, every agent run and its result, the pull request, people's decisions) and the events since its last turn. Then it answers with actions: a message to the requester, its checklist, agents to start (in parallel), follow-ups to an agent that already has the context, a question, spec approval, waiting for CI, the hand-off with a summary for the reviewer, or finishing without a code change. Kiln carries the actions out and wakes the foreman when an agent finishes, a person replies or decides, or CI finishes. Nothing runs while it waits.
foreman:
mode: agent # pipeline (default) or agent
maxTurns: 60 # turns per task before Kiln stops and asks a personKiln still opens the pull requests, applies the budgets and leaves the merge to people. After a restart the foreman hears which runs were cut off and picks up from there.
Blueprints
blueprint in factory.yaml replaces the foreman's own workflow with an explicit graph:
blueprint:
entry: draft
stages:
draft: { prompt: Write the change and its tests., outputs: The branch. }
check: { prompt: Review the change against the issue. }
edges:
to-check: { from: draft, to: check, when: the change is written }
staffing: { foreman: foreman, draft: implement, check: review }Each stage runs its staffed agent with the stage prompt. The agent can name the edge that holds (next in its result); with several edges and no answer the foreman decides; with one edge it is taken. A stage with no edge that holds ends the workflow, with the pull request handed off to a person.
Edges may loop back to an earlier stage. A stage that is entered more than five times stops the task as blocked with the reason "blueprint stage