# Introduction ## What Is Knecht? Knecht is a self-hosted dashboard that runs workflows on your projects. A workflow boots the project in an isolated environment on your server and works through a list of steps, for example a Composer update, the test suite, a link check, or an AI agent that looks into a bug report. The result comes back as a pull request, a preview URL, or a comment in the ticket that started it. It is aimed at agencies and freelancers that maintain many projects at once. The work it takes over is the recurring kind, security and dependency updates, link checks, and the first look at a bug report, where most of the effort goes into booting the right project. ## What Knecht Is Not - **Not an AI tool.** The AI action is one action among others. A workflow that updates packages, runs the tests, and opens a pull request needs no AI at all. - **Not a hosting platform.** Previews exist for review, sit behind the login, and go away with the session. Production stays where it is. - **Not a cloud service.** You need your own Linux server or a Mac, and you install and update Knecht yourself. During the beta we host a limited number of instances, see [Beta Testers](https://knecht.works/docs/resources/beta-testers). - **Not zero-effort.** A project has to be bootable by a machine. Projects with a [DDEV](https://ddev.com){rel=""nofollow""} config qualify directly. For PHP and Node projects without one, Knecht detects the stack and generates the config. ## Features | Feature | Explanation | | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Deterministic steps | Boot the project, run shell commands or your own JavaScript, call URLs, check links, create branches, commits, and pull requests, with conditions and loops. | | AI agent | Hands the booted project to an agent that reads code, changes files, and runs commands until the task is done. Keeps a memory per project. | | Triggers | GitHub events, changes in an [issue tracker](https://knecht.works/docs/usage/triggers#trigger-types), a schedule, or a click in the dashboard. One workflow can serve several projects. | | Mentions | `@knecht-works ` in a GitHub issue or pull request, or a mention of the Knecht account in an issue tracker, continues the work where the last run stopped. | | Previews | Every session gets a URL under your domain with a real database behind it, gated by the Knecht login. | | Terminal and IDE | Open a shell, an SSH command, or VS Code in the running environment from the run page. | | Retry | A failed run resumes from the step where it stopped, earlier outputs are kept. | | Environment lifecycle | Idle environments are stopped, archived, and removed on schedules you set. | | Team access | Members sign in with GitHub. An allowlist decides who gets in. | | Workflows as code | Export and import workflows as YAML. | | Updates | One install script, updates from the dashboard, optional auto-update. | ## Getting Started [Concepts](https://knecht.works/docs/get-started/concepts) explains how a run works and what projects, sessions, and previews are. [Installation](https://knecht.works/docs/get-started/installation) gets the instance up, [Setup](https://knecht.works/docs/get-started/setup) creates the GitHub App and invites the team, and [Projects](https://knecht.works/docs/usage/projects) adds the first project. The pages under Integrations describe what each integration can do, starting with [GitHub](https://knecht.works/docs/integrations/github). ## Community Knecht is young and built in the open, and the people who run it shape where it goes. If you try it, we would like to hear how it went. What worked, what broke, which workflow you wish existed, which stack refused to boot. Every report helps, and the small ones are often the most useful. - **Discord.** The [Knecht Discord](https://discord.gg/WuxjmtgUyX){rel=""nofollow""} is where questions get answered and ideas get discussed. Say hello, show what you built, or ask when something is unclear. - **GitHub.** Bugs and feature requests are welcome as issues on [GitHub](https://github.com/knecht-works){rel=""nofollow""}. The code is public. - **Email.** For anything else, write to . A real person reads it. What changes as a result shows up on the [Updates](https://knecht.works/updates) page. # Concepts A trigger starts a workflow on a project. Knecht boots the project in a session, runs the steps in order, and delivers what the last steps produced. This page explains each of these pieces, in the order a run touches them, and how the environments are isolated from each other. ![A project, a GitHub issue as trigger, a session with two runs and a follow-up mention, and a pull request and preview as result](https://knecht.works/assets/knecht-concepts.png) ## Projects A project is a GitHub repository connected to Knecht. If the repo already ships a `.ddev/config.yaml`, Knecht boots from it as it is. If not, Knecht reads `composer.json`, `package.json`, `.nvmrc`, and the lockfile, and generates the environment itself. Environment variables and a database dump are stored on the project, so every run starts from a working state. Adding and configuring projects is covered on [Projects](https://knecht.works/docs/usage/projects). ## Workflows A workflow is a list of steps Knecht runs on a project, in a fixed order. A typical one boots the project, updates the Composer packages, runs the tests, and opens a pull request. None of these steps need AI. Workflows live in the dashboard, and every instance ships with two starter workflows to copy from. Building and testing workflows is covered on [Workflows](https://knecht.works/docs/usage/workflows). ## Triggers A trigger defines when a workflow starts on its own. Knecht reacts to GitHub events such as a new issue or a label, to changes in a connected [issue tracker](https://knecht.works/docs/usage/triggers#trigger-types), and to a schedule. Any workflow can also be started by hand. One workflow can serve several projects. ## Actions Each step in a workflow executes one action. Deterministic actions boot the project, run a shell command or your own JavaScript, call a URL, check the site for broken links, or create a branch, a commit, and a pull request. Control flow actions run steps conditionally or in a loop. The AI action hands the booted project to an agent that reads code, changes files, and runs commands until the task is done. It is one action among the others, and a workflow works without it. The agent, its instructions, and its memory are covered on [AI Agent](https://knecht.works/docs/usage/agent). ## Sessions A session belongs to one GitHub issue or pull request, or to one issue in a connected issue tracker. It holds the checkout, the environment, and one shared agent conversation, so a follow-up mention continues where the last run stopped instead of starting over. The session closes with the issue and revives if the issue is reopened. Events without an issue, such as a schedule or a manual start, get a session of their own that closes after the single run. ## Runs A run is one execution of a workflow inside a session. Two triggers on the same issue run one after the other, never in parallel. A run has its own status, log, and steps, and a failed run can be retried from the step where it stopped. ![The project page with a succeeded run, the live preview in the middle, the follow-up chat below it, and the sessions and automation panels on the right](https://knecht.works/assets/knecht-run.png) ## Previews Every session gets a preview URL. It shows the current state of the environment, stays the same across all runs of the session, and needs a Knecht login to open. ## Isolation Every session runs in its own environment on your server. Nothing inside it can reach the host or another session. - **Own checkout and containers.** A session gets its own clone of the repository and its own [DDEV](https://ddev.com){rel=""nofollow""} project, with a separate web container, database container, and volumes. Two sessions of the same project never share files or a database. - **Commands run inside the container.** Shell commands, JavaScript steps, the agent, the terminal, and the IDE all execute inside the session's web container as an unprivileged user. The Docker socket and the DDEV CLI stay on the Knecht side, so a step cannot start containers or touch the host. - **Sessions cannot see each other.** The containers are detached from DDEV's shared network and attached only to Knecht's ingress network. No ports are published on the host. Previews go through Knecht's proxy, container to container, behind the login. - **Short-lived credentials.** Git pushes from a session use an installation token that is scoped to the one repository and expires after about an hour. The AI key reaches the agent process as an environment variable, not as a file in the checkout. ## Security On a self-hosted instance, Knecht does not manage your server's security or its updates. That stays your responsibility. On a managed instance we operate the server. The installer sets up Docker, DDEV, and Knecht itself, nothing more. Hardening the host, keeping the operating system patched, restricting SSH, and setting up a firewall beyond ports 80 and 443 are up to you. Knecht updates itself from the dashboard, the rest of the host it leaves alone. What Knecht does cover is the door into the instance. Sign-in goes through GitHub, only accounts on the member list get in, previews and the IDE sit behind that login, webhooks are signature-checked, and stored credentials are encrypted. Since the Knecht container drives the host's Docker daemon, the host should run nothing but Knecht. # Installation Knecht is installed on a server you control. One script sets up Docker, the [DDEV](https://ddev.com){rel=""nofollow""} CLI, and the app itself. This page ends when the dashboard is reachable under your domain. What happens on the first visit is covered in [Setup](https://knecht.works/docs/get-started/setup). ## Requirements ### Server A fresh server with root access, used only for Knecht. This can be: - A VPS, for example a [Hetzner](https://www.hetzner.com/cloud){rel=""nofollow""} CX23 or CX33 - A dedicated server or a virtual machine - A Mac, for example a Mac mini, through a Lima VM. See [Install on macOS](https://knecht.works/#install-on-macos). ::note Keep the host free of other Docker setups. Knecht boots the project environments on the host's Docker daemon and takes ports 80 and 443 for its own entry point. :: ### Operating System - Ubuntu 24.04 - amd64 or arm64 The installer stops on other distributions. Other Ubuntu versions are not tested. ### Hardware | | Minimum | Comfortable | | ---- | ------- | ----------- | | RAM | 4 GB | 8 GB | | Disk | 40 GB | 80 GB | Every running preview costs one web and one database container. More parallel previews need more RAM. ### Domain and Network - A domain or subdomain for the instance, for example `knecht.example.com` - Access to its DNS, for two records you set after the install - Ports 80 and 443 reachable from the internet ## Install on Linux As root on the fresh server: ```bash curl -fsSL https://raw.githubusercontent.com/knecht-works/knecht-cloud/main/scripts/install.sh | bash ``` The installer asks for your domain and does the rest. It installs Docker and the pinned DDEV CLI, warms up the DDEV images, checks out the latest release under `/opt/knecht`, writes a `.env`, and starts the app together with the Caddy TLS entry point via Docker Compose. It takes a few minutes and is safe to re-run if something breaks halfway. ::note For a non-interactive install, pass the domain up front with `KNECHT_DOMAIN=knecht.example.com bash`. Any further `KNECHT_*` variable, for example `KNECHT_AI_KEY`, is carried into the `.env` and locks that setting in the dashboard. :: ::steps{level="3"} ### Set the DNS Records Two `A` records, both pointing at the server's IP: ```text knecht.example.com *.preview.knecht.example.com ``` The wildcard serves the preview URLs. Every session gets its own hostname below `preview`. ### Open Ports 80 and 443 Both must be reachable from the internet. On a cloud VPS this usually means a rule in the provider's firewall. ### Open the Dashboard Visit `https://knecht.example.com`. Caddy fetches the certificate on first start, so the first request can take a moment. The page that opens is the GitHub App setup, continued in [Setup](https://knecht.works/docs/get-started/setup). :: ## Install on macOS The run substrate, host Docker plus DDEV, is Linux only. On a Mac the same setup runs inside a [Lima](https://lima-vm.io){rel=""nofollow""} VM. The template from the repo gives the VM 4 CPUs, 8 GB RAM, and 80 GB disk, and forwards ports 80 and 443 on all interfaces of the Mac. ::steps{level="3"} ### Create the VM ```bash brew install lima limactl create --name=knecht https://raw.githubusercontent.com/knecht-works/knecht-cloud/main/scripts/lima-server.yaml limactl start knecht ``` ### Run the Installer inside the VM ```bash limactl shell knecht curl -fsSL https://raw.githubusercontent.com/knecht-works/knecht-cloud/main/scripts/install.sh | sudo bash ``` The installer asks for the domain, the same as on Linux. ### Make the Mac Reachable The three steps from the Linux install apply unchanged: DNS records, ports, dashboard. On a home network, two things come on top: - Forward TCP ports 80 and 443 on your router to the Mac, and give the Mac a fixed address in the router. - Home connections change their public IP over time. Point the two DNS records at a dynamic DNS name, or get a static IP from your ISP. :: ### After a Reboot The VM does not start on its own. Run `limactl start knecht` after a macOS reboot, the containers inside come back up by themselves. `/opt/knecht` and `/data/knecht` live inside the VM, reachable through `limactl shell knecht`. ### Local Domain for Testing To try Knecht on a Mac without any DNS setup, install it under a local domain. GitHub cannot reach the instance then, so webhooks never arrive and GitHub triggers do not fire. Manual and scheduled triggers work. ::steps{level="4"} #### Use lvh.me as the domain `lvh.me` is a public domain that, together with all its subdomains, always resolves to `127.0.0.1`. It needs no DNS setup and no entries in `/etc/hosts`. Enter `lvh.me` when the installer asks for the domain, then the dashboard and every preview subdomain work locally. Ports 80 and 443 on the Mac must be free. #### Switch Caddy to its internal CA Let's Encrypt cannot issue certificates for a local domain. Inside the VM, edit `/opt/knecht/Caddyfile` so both site blocks use `tls internal`: ```text {$KNECHT_BASE_DOMAIN} { tls internal reverse_proxy knecht:3000 } https:// { tls internal { on_demand } reverse_proxy knecht:3000 } ``` #### Restart Caddy and open the dashboard ```bash cd /opt/knecht && sudo docker compose restart caddy ``` Open `https://lvh.me` and accept the certificate warning. :: Once the dashboard opens, continue with [Setup](https://knecht.works/docs/get-started/setup). Updating, backups, and pre-releases are on [Maintenance](https://knecht.works/docs/get-started/maintenance). # Setup The first visit to a fresh instance lands on the setup page. It creates the GitHub App that Knecht signs in with and reads repositories through. This page walks through that flow, then through the settings you need before the first project: who can sign in and the AI provider. Every issue tracker has its own page under Integrations. ::tip A community walkthrough of the installation and setup is on [Matthias Andrasch's blog](https://matthias-andrasch.eu/blog/2026/exploring-knecht-cloud-for-ddev-ai-installation-part-1/){rel=""nofollow""}. :: ## Creating the GitHub App The setup page has one button, "Create GitHub App". Knecht does not ask for client IDs or secrets. It sends a prefilled manifest to GitHub and gets the credentials back. The app is created under the account that clicks the button, and that account becomes the owner of the instance. ::steps{level="3"} ### Confirm the App on GitHub GitHub shows the prefilled app with the name `Knecht `. You can edit the name, everything else is set by the manifest. The app requests write access to contents, pull requests, and issues, read access to metadata, and subscribes to pull request, issue, and issue comment events. Submit the form. ### Install the App on Your Repositories GitHub hands the credentials to Knecht, which stores them encrypted and sends you straight to the installation page. Pick the account or organization, then either all repositories or a selection. The app is public, so it can be installed on any organization you administer, not only on the account that created it. Repositories can be added later in the app's settings on GitHub. ### Sign In After the installation, GitHub redirects to the login page of your instance. Sign in with GitHub. The same app handles login, so there is no separate OAuth setup. You land in the dashboard as the owner. :: ::note The setup runs once. When an app exists, the setup page only offers the login. To connect a different app, you would have to reinstall the instance. :: ## Inviting Your Team Only GitHub accounts on the member list can sign in. Everyone else sees "Access denied" after the GitHub login. The list is under Settings > Access. Enter a GitHub login and click "Invite". The account can sign in right away, its name and avatar show up after the first login. ![The Access page under Settings, with the current members and an invite field for a new GitHub username](https://knecht.works/assets/knecht-settings-user.png) Every member has the same access as the owner, including inviting and removing members. The owner is the account that created the GitHub App and is the only member that cannot be removed. ## Connecting Integrations The AI provider is required for the AI action and lives under Settings, Agent. Issue trackers are optional and live under Settings, Integrations. ### AI Provider The AI action runs an agent inside the run's environment, authenticated against one provider with one key. Under Settings > Agent, pick the provider, paste the key, and choose a default model. The key is stored encrypted and is not shown again. Supported providers: - OpenCode Zen and OpenCode Go, two separate plans. Both take a service account key (`oc_sk_…`) from the [OpenCode console](https://opencode.ai/console){rel=""nofollow""}. Pick the plan you pay for, the model lists differ. - Anthropic, OpenAI, and Google with their own API keys. - Langdock, a gateway with an EU or US region. The region applies to every request of the instance. The model list comes from the selected provider. For OpenCode it comes from your workspace, so models disabled there do not show up. The optional subtask model is used when the agent delegates smaller tasks. Each AI step in a workflow can override the default model. The instructions field on the same page is covered on [AI Agent](https://knecht.works/docs/usage/agent). ![The Agent page under Settings, with the provider, API key, and default and subtask model fields](https://knecht.works/assets/knecht-settings-ai.png) ::tip Beta testers get OpenCode credit from us. How to get the key and where to paste it is on [Beta Testers](https://knecht.works/docs/resources/beta-testers#getting-your-opencode-credit). :: If the installation preset the key, for example through `KNECHT_AI_KEY`, the field is locked in the dashboard and shows "Preset by the installation". ### Issue Trackers (Optional) Issues in a connected issue tracker can start workflows, the agent answers on them, and finished runs comment the pull request link back on them. Each connection needs an API token, a webhook registered in the tool, and a link between each project and its counterpart there. Which tools are supported is listed under [Trigger Types](https://knecht.works/docs/usage/triggers#trigger-types), and the Setup section of each tool's page walks through all of it. # Maintenance A self-hosted instance needs three things from its operator: updates, a backup, and an eye on certificates. All of it happens on the server, on the System page of the dashboard, or under Settings, Advanced. Managed beta instances are maintained by us, so this page only applies to self-hosted installs. ## Updating Releases are git tags in the form `vX.Y.Z`, built by CI into a Docker image. When a newer release exists, the System page shows its release notes and an update button. Automatic updates are set under Settings, Advanced. ### Manual Updates The System page shows an "Update to vX.Y.Z" button. It swaps the app to the new version with all data intact and takes about a minute, during which the dashboard is unreachable. Only the owner can start it, because it changes the running code for every member. ::note The manual equivalent on the server, for example when the dashboard is down: ```bash cd /opt/knecht git fetch --tags && git checkout vX.Y.Z sed -i 's/^KNECHT_VERSION=.*/KNECHT_VERSION=vX.Y.Z/' .env docker compose pull && docker compose up -d ``` :: ### Automatic Updates Under Settings, Advanced, the panel "Automatic updates" has a field for a cron expression in server time, for example `0 3 * * *` for nightly at 03:00. Once set, Knecht updates itself as soon as a new release exists and no run is active. Leave the field empty to update by hand. ### Host-Level Updates The in-app update covers the app only. Docker, the [DDEV](https://ddev.com){rel=""nofollow""} CLI, and the DDEV image warm-up live on the host and are not touched. When a release needs a newer DDEV or a changed host setup, the release notes say so, and the provisioning script has to be re-run once: ```bash sudo KNECHT_UID=1000 KNECHT_GID=1000 /opt/knecht/scripts/provision-host.sh ``` The script is idempotent and safe to run at any time. ### Pre-Releases Tags with a hyphen, such as `v0.3.0-rc.1`, are pre-releases. CI builds them like any release, but they are never offered as an update and a normal install ignores them. To test one, fetch the installer from that tag and pin the same tag with `KNECHT_REF`, so installer and version cannot diverge: ```bash curl -fsSL https://raw.githubusercontent.com/knecht-works/knecht-cloud/v0.3.0-rc.1/scripts/install.sh \ | sudo env KNECHT_DOMAIN=knecht.example.com KNECHT_REF=v0.3.0-rc.1 bash ``` Once the matching stable release is published, the instance offers it as a regular update. Updating from a stable version to a pre-release is not possible. ## Backup and Rollback All state lives in `/data/knecht/data`: the SQLite database, run archives, and uploaded database dumps. Back that folder up, or snapshot the whole server. Project checkouts under `/data/knecht/projects` are disposable, Knecht clones them again when needed. ::warning Stored secrets, such as the GitHub App credentials, the AI key, and the tokens of connected integrations, are encrypted with a key derived from `NUXT_SESSION_PASSWORD` in `/opt/knecht/.env`. A backup of the data folder alone cannot be restored without that file. Keep a copy of the `.env` as well, ideally separate from the data backup. :: Database migrations run forward only. Checking out an older release does not roll the schema back. Take a copy of `/data/knecht/data` before an update if you want a safe way back, and restore the copy together with the older tag. On a Mac, both folders live inside the Lima VM. Reach them through `limactl shell knecht`, or back up the VM disk as a whole. ## Certificates Caddy handles TLS on its own. It fetches the certificate for the dashboard on first start and one certificate per preview hostname on demand, so the first visit of a new preview URL takes a few extra seconds. Nothing needs renewing by hand. Let's Encrypt issues at most 50 new certificates per week per domain. Each newly visited preview hostname uses one, renewals do not. A team with many sessions per week can hit that limit. The way out is a wildcard certificate for the preview zone: 1. Delegate the preview subzone to a DNS provider with an API, for example `preview.knecht.example.com NS -> Hetzner DNS`. The main zone stays where it is. 2. Build Caddy with the matching DNS plugin. 3. In `/opt/knecht/Caddyfile`, replace `on_demand` with `tls { dns }`. # Projects A project is a GitHub repository connected to Knecht, together with what it needs to boot: environment variables, a database dump, and a few settings. This page covers what kind of projects work, how to add one, and what you can do with it afterward. ## Supported Stacks Knecht boots projects with [DDEV](https://ddev.com){rel=""nofollow""}. These are tested on real projects and run in our test suite: ::docs-stack-grid :::docs-stack-item{icon="i-simple-icons-craftcms" to="https://craftcms.com"} Craft CMS ::: :::docs-stack-item{icon="i-simple-icons-drupal" to="https://www.drupal.org"} Drupal 10 and 11 ::: :::docs-stack-item{icon="i-simple-icons-kirby" to="https://getkirby.com"} Kirby ::: :::docs-stack-item{icon="i-simple-icons-laravel" to="https://laravel.com"} Laravel ::: :::docs-stack-item{icon="i-simple-icons-typo3" to="https://typo3.org"} TYPO3 ::: :::docs-stack-item{icon="i-simple-icons-php" to="https://www.php.net"} Plain PHP with Vite ::: :: Any other repository is expected to work too. A `.ddev/config.yaml` boots from that file as it is, with the framework, PHP version, and database read straight from it. Without one, Knecht generates an environment from: - The PHP version from `composer.json`. - The Node version from `.nvmrc`, `engines.node`, a mise config, or `.tool-versions`. - The package manager from the lockfile. ::note If a project outside the tested list misbehaves, that is a bug worth reporting. One known difference to local DDEV concerns Vite dev servers, which in some setups need one line of config. The cases are collected on [Troubleshooting](https://knecht.works/docs/resources/troubleshooting). :: ## Adding a Project "New project" on the projects page opens a short guided setup. The project exists after the first step, everything after that is optional and can be changed later in the project settings. ::steps{level="3"} ### Connect the Repository Pick a repository from those the GitHub App is installed on, and a branch. It defaults to the repository's default branch. Runs check out this branch and open pull requests against it. The branch is fixed per project, another branch can be chosen per run. ### Set Environment Variables Paste the project's `.env`, one `KEY=value` per line, or skip and add them later. :::tip Values can use `$KNECHT_PREVIEW_URL` and `$KNECHT_DEV_SERVER_URL`. Knecht fills them per run, so a variable such as `APP_URL=$KNECHT_PREVIEW_URL` always points at the current preview. ::: ### Import a Database Upload a dump as `.sql`, `.gz`, `.zip`, `.bz2`, `.xz`, `.tar`, or `.mysql`. It is imported into the environment on the first boot and stays there for later runs. Without a dump, the project boots with an empty database. ### Boot and Preview The last step offers "Open project" and, as long as the bundled `boot-and-preview` workflow exists, "Boot & preview". Boot runs that workflow right away: it imports the database, builds the project, and opens a preview so you can confirm the project comes up. Open project skips that and shows the project page, where the same can be started later. :: ## Working with a Project Everything from the guided setup lives in the project settings and can be changed there. "Settings" in the top right of the project page opens them. Beyond the setup values, the settings hold the Mentions panel with the starter workflow, a link panel for each connected issue tracker, and "Persistent folders" for files that should survive across runs. ![The project settings page, with panels for env variables, agent instructions, environment, boot commands, dev server, and database dump](https://knecht.works/assets/knecht-project-settings.png) # Workflows A workflow is a list of steps Knecht runs on a project, one after the other, inside the project's own environment. Each step executes one action. A workflow is built once and can then be started by hand or by a trigger on any number of projects. ## Building a Workflow The workflow editor shows the steps as a numbered rail. Steps are added from the library, reordered by drag and drop, and configured in place. Every edit is saved automatically. Steps run in order, there are no parallel branches. ![The workflow editor for a Summarize PR workflow, with Run in the header, a GitHub pull request trigger with its events, conditions, and projects, a Boot project and an AI step, and the step library on the right](https://knecht.works/assets/knecht-workflow-ki-summarize.png) ### Run Variables Steps reference earlier results and trigger data with double curly braces. Typing `{{` in any field opens an autocomplete with what is available at that point. The namespaces: | Run variable | Holds | | ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `{{ steps.. }}` | An output of an earlier step, for example `{{ steps.update.stdout }}`. The id is derived from the step's label and editable. | | `{{ inputs. }}` | What the trigger delivered: `title`, `body`, `url`, `identifier` (the issue or pull request number, or the ticket key), plus `event`, `status`, `labels`, `assignee`, and `author`. | | `{{ run.id }}` and `{{ run.url }}` | The run's number and its page in the dashboard. | | `{{ project.name }}` | The project's name, also `owner`, `fullName`, and `defaultBranch`. | | `{{ preview.url }}` | The preview URL after the boot step. The boot step also delivers it as its output `{{ steps..url }}`, which is the form the autocomplete offers. | | `{{ loop.item }}`, `{{ loop.index }}` | Inside a Loop step, the current item and its index, starting at 0. | A missing value renders as an empty string, so a run variable for something a step has not produced yet does not break the run. ## Triggers Triggers sit at the head of the workflow in the editor and define when it starts on its own. Knecht offers GitHub events, issue trackers, and cron schedules. Starting a workflow by hand needs no trigger. "Pause triggers" in the menu of the editor header turns all triggers of a workflow off, and a paused workflow says so above its triggers. Each trigger, its options, and the inputs it delivers are described on [Triggers](https://knecht.works/docs/usage/triggers). The steps and agent commands that write back are described on the page of each integration. ## Testing before You Activate "Run" in the editor header starts the workflow on a project and branch of your choice, with the current draft. The test runs under the same rules as a real run, in the project's environment, with the log streaming into the editor. A collapsed section in the run popover lets you type a mock trigger event, so run variables such as `{{ inputs.title }}` can be tried without a real issue. The editor remembers the last mock inputs per workflow in your browser. A draft that is not complete cannot run. The header lists what is missing, and clicking an entry opens the affected step. ## Reporting Back When a run in the session of an issue tracker issue finishes, Knecht comments on that issue without any step in the workflow: - **A pull request was opened.** "Knecht opened a pull request for this issue" with the link. The tool's own word stands in for "issue", for example "ticket" in Jira. - **The run failed.** "Knecht could not finish the run for this issue" with the link to the run in the dashboard. - **Succeeded without a pull request.** Nothing. Whatever the agent replied stands on its own. Runs on a GitHub issue or pull request get no such comment. ## Workflows as Code The overflow menu in the editor exports a workflow as YAML or JSON. "Import" on the workflows page creates a workflow from such a file. The export format is a plain step list, so a workflow can live in a git repository, be reviewed like code, and be moved between instances. Hand-written files are accepted as well and can use the short form: ::docs-workflow-yaml{filename="demo-pr.yaml"} ```yaml version: 1 name: demo-pr description: Make a small change and open a PR. steps: - create-branch: name: knecht/demo-{{ run.id }} - bash: command: date > .knecht-demo.txt - create-commit: message: "Knecht demo change (run {{ run.id }})" - create-pr: title: "Knecht demo (run {{ run.id }})" description: | Automated demo change by Knecht, run {{ run.id }} on {{ project.name }}. ``` :: # Triggers A trigger starts a workflow without anyone pressing "Run". It sits at the head of the workflow in the editor, targets one or more projects, and starts a run each time it fires. A workflow can have any number of triggers, also several of the same kind. Starting a workflow by hand needs no trigger, see [Testing before You Activate](https://knecht.works/docs/usage/workflows#testing-before-you-activate). ## Trigger Types | Type | Starts a run when | Details | | -------- | --------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | | Schedule | A cron expression is due, for example every Monday at 09:00. | [Schedule](https://knecht.works/#schedule) below | | GitHub | Something happens on a pull request or an issue: opened, ready for review, new commits, a label, an assignment. | [GitHub](https://knecht.works/docs/integrations/github#triggers) | | Jira | Something happens on a ticket: created, assigned to Knecht, a label, a status. | [Jira](https://knecht.works/docs/integrations/jira#triggers) | | Plane | Something happens on a work item: created, assigned to Knecht, a label, a state. | [Plane](https://knecht.works/docs/integrations/plane#triggers) | | Linear | Something happens on an issue: created, assigned to Knecht, a label, a status. | [Linear](https://knecht.works/docs/integrations/linear#triggers) | This table is the list of sources. Each one has its own page with its events, conditions, and setup, and everything on this page applies to all of them. GitHub works on every instance. An issue tracker has to be connected first, the Setup section of its page walks through it. Until then, the dialog shows a hint with the way to Settings, Integrations instead of the form. All sources except the schedule arrive through webhooks, so they need an instance that is reachable from the internet. ## Adding a Trigger "Add trigger" below the triggers of a workflow opens the dialog. Pick the source, one or more projects, and what the source should react to. For an issue tracker, only projects that are linked to their counterpart in the tool are listed. The link is set in the project settings. ![The trigger dialog with Linear selected as source, one linked project, the event Created ticked, and the condition Status is Any Triage](https://knecht.works/assets/knecht-trigger-linear.png) Once saved, the trigger appears as a row at the head of the workflow. The row names the source and reads like a sentence, for example "on opened, ready for review, pushed" and below it "if base branch `main` and author not `renovate[bot]`". The projects stand as chips on the right. A click on the row opens the dialog again. The switch on the row pauses that one trigger, the menu next to it deletes it. ### Events and Conditions All triggers except the schedule share one form with two sections. - **Fires when any of these happens.** The events. Any ticked event fires the trigger. Some take values, such as the labels of "Label added". An event with several values fires on any of them. - **But only if.** The conditions. Each one names a field, "is" or "is not", and one or more values. "is" holds when the object has one of the values, "is not" when it has none of them. "Add condition" starts the first group. "and" adds a condition to a group, and every condition of a group has to hold. "or group" adds another group, and the trigger fires when one of the groups holds. A pull request trigger with the events "Opened" and "Label added", one group "base branch is `main` and author is not `renovate[bot]`", and a second group "label is `urgent`" fires on either event for pull requests against `main` that the bot did not open, and for every pull request labeled `urgent`. Which events and fields a source has is listed on its page, see [Trigger Types](https://knecht.works/#trigger-types). ### Matching The values of events and conditions follow the same rules in every source. - **Picked from the tool.** Labels, statuses, issue types, priorities, and the draft state of a pull request are picked from a list, read live from the tool. They are compared exactly, and a value that is not in the list cannot be typed. - **Patterns.** Branches and GitHub logins are typed, with the branches and assignees of the repository as suggestions. A condition on them takes a pattern: `*` stands for any text, so `releases/*` matches `releases/v1` and `*[bot]` every bot account. Without a `*` the value has to match as a whole. The login of the event "Assigned to" is no pattern and is matched exactly. - **Upper and lower case count.** `Bug` does not match `bug`. This holds for every value, logins included. With several projects selected, labels and statuses that exist in only some of them are listed under "Only in some", with the keys of their counterparts in the tool. If a picked value of an event is missing in one of the projects, the form warns that nothing there will ever fire the trigger. Pick a value all projects share, or create a second trigger. ### Issue Trackers Issue trackers share the events "Created", "Assigned to Knecht", "Label added", and an event for a status being reached. An issue that is created with the label or already assigned to Knecht fires as well, without "Created" being ticked. An issue that is created in a status does not reach it, the status event only fires on a move. The status list has two groups. - **Category.** An entry such as "Any Done" stands for a whole status category of the tool, whatever the single statuses are called. The event fires when an issue crosses into the category. A move inside it, for example from "In Progress" to "In Review", does not fire. - **Exact status.** The statuses of the selected projects, read live from the tool. The event fires every time an issue moves into that status. The same list serves the condition "Status", which checks the status the issue is in right now. "Created" with the condition "Status is Any Triage" fires for new issues that land in triage, "Status is not Any Done" keeps closed issues out. The condition "Assignee is Knecht" holds while the issue is assigned to the account behind the connection. #### Knecht Holds the Issue While a run or a mention works on an issue, the issue is assigned to the Knecht account, like a colleague who picks it up. Once the session has no more work to do, Knecht hands it back: to the person whose change started the run, else to whoever held the issue before Knecht, else to whoever created it. A cancelled run hands back as well. If somebody took the issue from Knecht in the meantime, it stays with them. Knecht taking the issue itself does not fire "Assigned to Knecht". Labels and statuses the agent sets fire triggers as usual, and the condition "Assignee is Knecht" holds while Knecht works on the issue. Plane, where a work item takes several assignees, differs a little, see [Plane](https://knecht.works/docs/integrations/plane#knecht-holds-the-work-item). ## How Triggers Behave - **Pausing.** "Pause triggers" in the menu of the editor header turns all triggers of a workflow off. A paused workflow shows "Paused: triggers won't fire" above its triggers, with a switch that turns them on again. Each trigger also has its own switch. Manual runs are not affected by either. - **Last complete version.** Triggers run the last version of the workflow that passed validation. An incomplete edit stays a draft and the previous complete version keeps running. - **One run per project.** A schedule starts one run for every project of the trigger. Every other event only runs on the project whose repository or linked counterpart in the issue tracker sent it. - **One run per change.** An issue or pull request that is created or edited with several changes at once, for example opened with a label and an assignee, starts one run, not one per change. - **Sessions.** Runs on a GitHub issue or pull request or on an issue of an issue tracker join its [session](https://knecht.works/docs/get-started/concepts#sessions), so two triggers firing on the same issue run one after the other. Scheduled and manual runs get a session of their own that closes after the run. - **Inputs.** All triggers except the schedule fill the run variables `{{ inputs.identifier }}`, `{{ inputs.title }}`, `{{ inputs.body }}`, `{{ inputs.url }}`, `{{ inputs.event }}`, `{{ inputs.status }}`, `{{ inputs.labels }}`, `{{ inputs.assignee }}`, and `{{ inputs.author }}`. A field the event does not have arrives as an empty string. A schedule delivers no inputs. ## Schedule A schedule fires on a standard five-field cron expression: minute, hour, day of month, month, day of week. The form offers presets from every 15 minutes to weekly on Monday at 09:00. ![The trigger dialog with Schedule selected as source, three projects as chips, the cron expression for daily at 09:00, and the presets below the field](https://knecht.works/assets/knecht-trigger-schedule.png) Times are in the server's local time. Supported are `*`, steps like `*/15`, ranges like `1-5`, and lists like `1,15`. Seconds and aliases like `@daily` are not supported. [crontab.guru](https://crontab.guru){rel=""nofollow""} explains the format and translates an expression into plain words. If the instance was down when a slot passed, the schedule fires once after the restart, not once per missed slot. # AI Agent The AI step starts [opencode](https://opencode.ai){rel=""nofollow""} inside the project's environment with your provider key. It is an agent, not a chat: it reads the code, changes files, runs commands, and checks its own work until the task in the prompt is done. This page covers where it runs, how to steer it, and what it hands back to the workflow. ## Where It Runs The agent runs inside the web container of the booted project, as a non-root user, on the run's checkout. Composer, npm, and git are there, so it behaves like a developer on the real project. What it cannot reach: - **Docker and the host.** The Docker socket and the [DDEV](https://ddev.com){rel=""nofollow""} CLI stay on the Knecht side. The agent only ever sees the inside of its container. - **Other runs.** Each environment sits on its own network, parallel runs cannot reach each other. - **Secrets.** The provider key is handed to the process as an environment variable and never written into the checkout. Git pushes go through a short-lived token scoped to the session's one repository. A small set of Knecht commands is available to the agent inside the container: git with credentials for the project's repository, a command to push the branch and open a pull request, and, when the session belongs to a GitHub issue or pull request or to an issue of an issue tracker, commands to read the thread, post a reply, add or remove existing labels, and, in an issue tracker, move it to another status. What each of them does is on the page of the integration, in the section about the agent, for example on [GitHub](https://knecht.works/docs/integrations/github#the-agent-on-issues-and-pull-requests). ## Instructions Three layers of instructions apply, in this order: 1. **Built-in rules.** Knecht ships its own rules for working style, git usage, memory, and how to reply on a thread. They are the same on every instance. 2. **Instance instructions.** Settings > Agent has a free text field that applies to every project, for example "Always answer in German" or "Never touch anything under legacy/". 3. **Project instructions.** The project settings have the same field for rules that apply to this project only, for example where the styles live or which design tokens to use. On top of that, an AI step can carry its own step prompt, a task framing for that one step. A repository's own `AGENTS.md` is read by opencode as usual. ## Memory The agent keeps notes per project, so it does not rediscover the same facts on every run: how the project builds, where the styles live, corrections from earlier follow-ups. The notes live on the host outside every environment and are copied into the checkout before each agent call and back afterwards. The structure is an index file, `MEMORY.md`, that is part of the agent's instructions on every call, plus topic files the agent reads on demand. The agent writes and curates the notes itself, guided by the built-in rules. The index is capped at 2 KB and the whole store at 64 KB. An oversized write is discarded and the previous state kept. ::note There is no view of the notes in the dashboard yet. :: ## Output Fields By default, an AI step returns the agent's final text under `{{ steps..text }}`. For later steps that need specific values, the step's "Output format" field declares them, one per line: ```text prTitle: string labels: string[] needsReview: boolean ``` The allowed types are `string`, `number`, `boolean`, and their array forms. The agent writes the result as a file, Knecht validates it against the declaration and asks the agent to fix it if it does not match. Each field is then available as `{{ steps..json. }}`, for example as the title of the pull request step. ## Models The default model and an optional subtask model are set under Settings > Agent. The subtask model is used when the agent delegates smaller pieces of work, for example exploring the repository. Each AI step has a model field that overrides the default, so one workflow can mix a small model for triage and a large one for the fix, all behind the same key. Switching the provider clears both stored models, because model names do not carry over between providers. Pick them again after the switch. ## Follow-Ups and Mentions The conversation does not end with the run. Every session has one agent conversation, and every run and follow-up in that session continues it, so later work sees what earlier work found. Two ways to continue: - The follow-up chat on the run page sends a new prompt into the session's environment. - A mention in a comment does the same from GitHub or an issue tracker, see [Mentions](https://knecht.works/#mentions) below. ![The follow-up panel on the run page, with a request to add a dark mode, the agent's reply with its collapsed tool calls, and the input field for the next message with the model picker](https://knecht.works/assets/knecht-run-follow-up.png) Follow-ups in one session run one after the other. A stopped environment is restarted for the follow-up, an archived one is restored first. ::note For a walkthrough of setting this up end to end, see [Exploring Knecht Cloud: Talking to an AI Agent in GitHub Issues](https://matthias-andrasch.eu/blog/2026/exploring-knecht-cloud-talking-to-an-ai-agent-in-github-issues-part-2/){rel=""nofollow""}. :: ### Mentions A mention is a comment that addresses Knecht. Knecht runs the text of the comment as a follow-up and posts the answer as a comment in the same thread. **Set it up once per project.** 1. Open the project settings and go to the Mentions panel. 2. Pick a "Starter workflow" and make sure it is published. It boots the environment when a mention arrives on an issue Knecht has not worked on yet, so a workflow with a boot step is the right choice. **Mention Knecht.** | Where | Write | | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | GitHub issue or pull request | `@knecht-works fix the broken footer link` | | Issue tracker | Mention the account Knecht is connected with, the way you mention a colleague, followed by the instruction. `@knecht` typed as plain text works as well. | The issue has to belong to a project in Knecht: its repository on GitHub, or the project linked to it in an issue tracker. **What happens.** 1. Knecht looks up the session of the issue. If there is none, or its environment is down, the starter workflow runs first. 2. The comment runs as a follow-up in that environment, with the agent conversation of the session. 3. The answer arrives as a comment. If a pull request was opened, its link is part of it. **If nothing happens.** | Knecht answers | Reason | | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | With a setup hint | No starter workflow is chosen, or it is not published. | | Not at all | "Answer mentions in this repo" is off in the Mentions panel under Advanced, the issue belongs to no project, or the comment comes from an account that may not mention Knecht, on GitHub one that is not listed under Settings, Access. | Who may mention Knecht depends on the tool: on GitHub only the accounts that can sign in to your Knecht dashboard (Settings, Access), in an issue tracker everyone who can comment on the issue. Comments by Knecht itself are always ignored. The Mentions section on the page of each integration has the details. # GitHub GitHub is the integration every instance has. The GitHub App created during setup signs your team in, lists the repositories that become projects, delivers the events that start workflows, and lets the agent push branches, open pull requests, and answer on issues. This page collects what that covers and how to use it in workflows. ## Setup The GitHub App is created once, on the setup page of a fresh instance. [Setup](https://knecht.works/docs/get-started/setup#creating-the-github-app) walks through that flow. Once the app is installed on a repository, that repository is ready. There is nothing to configure per repository, no webhook and no token. What the app is allowed to do on the repositories it is installed on: | Access | Used for | | -------------------- | -------------------------------------------------------------------------------------------------------- | | Contents, write | Checking out the code and pushing branches | | Pull requests, write | Opening pull requests | | Issues, write | Reading issues, posting comments, and setting labels. Comments on pull requests go through the same API. | | Metadata, read | Listing repositories and branches | It subscribes to three webhook events: pull request, issues, and issue comment. Everything on this page is built from those. ## Capabilities ### Triggers A GitHub trigger watches the repositories of the selected projects and starts one run for the project whose repository sent the event. The tabs "Pull request" and "Issue" at the top of the form decide what the trigger is about. Below them, "Fires when any of these happens" lists the events and "But only if" the conditions. Any ticked event fires the trigger, and one group of conditions has to hold. How the two sections work in general is on [Triggers](https://knecht.works/docs/usage/triggers#events-and-conditions). ![The trigger dialog with GitHub selected as source, two projects as chips, the Pull request tab, the events Opened, Ready for review, and New commits pushed ticked, the condition Base branch is main, and an or group with Label is bug](https://knecht.works/assets/knecht-trigger-github.png) #### Pull Request Events | Event | Fires when | | ------------------ | --------------------------------------------------------------------------------------------------------------- | | Opened | A pull request is opened or reopened. Ticked by default. | | Ready for review | A draft is marked as ready for review. | | New commits pushed | Commits are pushed to the pull request, force pushes included. Every push starts a run. | | Label added | One of the picked labels is added to the pull request. The labels are picked from the labels of the repository. | Runs check out the pull request's branch. #### Pull Request Conditions | Field | Compares against | | ----------- | ---------------------------------------------------------------------------------- | | Base branch | The branch the pull request targets, for example `main` or `releases/*`. | | Head branch | The pull request's own branch, for example `renovate/*` for every Renovate branch. | | Author | The login that opened the pull request. | | Assignee | The logins the pull request is assigned to. | | Label | The labels on the pull request. | | PR state | "Draft" or "Ready for review". | Each condition takes "is" or "is not". Branches and logins are typed, with the branches and assignees of the repository as suggestions, and take patterns. Bots are written the way GitHub shows them, for example `renovate[bot]`. Labels and the PR state are picked from a list. Upper and lower case count, in logins too. The rules are the same for every source, see [Matching](https://knecht.works/docs/usage/triggers#matching). ::tip A pull request that is opened as a draft never passes "PR state is Ready for review" with the Opened event alone. Tick "Ready for review" as well, and the run starts the moment the draft is released. :: #### Issue Events and Conditions ![The trigger dialog on the Issue tab, with the events Opened and Label added with the labels bug and enhancement ticked, and the condition Author is not \*bot](https://knecht.works/assets/knecht-trigger-github-issue.png) | Event | Fires when | | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Opened | A new issue is opened, or a closed one is reopened. Ticked by default. | | Label added | One of the picked labels is added to the issue. The labels are picked from the labels of the repository. | | Assigned to | The issue is assigned to one of the named GitHub logins. The assignees of the repository are suggested, other logins can be typed. The login has to match exactly, upper and lower case included. | Issues take the conditions "Author", "Assignee", and "Label", with the same rules as on pull requests. Runs check out the project's default branch. #### Inputs Both kinds fill the same run inputs: `identifier` is the number, plus `title`, `body`, `url`, `status`, `labels`, `assignee`, and `author`. `event` is `pull_request` or `issues`. Pausing, versioning, and the shared behavior of all triggers are on [Triggers](https://knecht.works/docs/usage/triggers). ### Sessions on Issues and Pull Requests A run started by an issue or pull request joins the [session](https://knecht.works/docs/get-started/concepts#sessions) of that issue or pull request. The session keeps the checkout, the environment, and one agent conversation, so a later trigger or a mention continues where the last run stopped. Closing the issue closes the session, reopening it revives the session. Scheduled and manual runs have no issue behind them and get a session of their own that closes after the run. ### Workflow Steps Three steps in the Output group of the step library work with GitHub. They run in any workflow, no matter whether a GitHub event, a schedule, or a click started it. | Step | Fields | Outputs | | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | | Create branch | Branch name, for example `knecht/{{ run.id }}`. Branches off the current checkout. | `{{ steps..name }}` | | Create commit | Commit message. Commits everything the run changed under the app's bot account. Nothing to commit is skipped, not an error. | `{{ steps..sha }}`, empty when nothing changed | | Pull request | Title and Description. Pushes the branch and opens a pull request against the project's default branch. Needs a Create branch step earlier in the workflow. | `{{ steps..url }}` and `{{ steps..number }}` | Knecht appends a link to the preview environment to every pull request description. A branch without commits is skipped without failing, and the step then produces no outputs. The run page shows an "Open Pull Request" button once a pull request exists. ### The Agent on Issues and Pull Requests When the run's session belongs to an issue or pull request, the [agent](https://knecht.works/docs/usage/agent) gets four commands inside the environment. The prompt of an AI step only has to name what to do with them. - **Read the thread.** Title, state, author, assignees, labels, body, and the last ten comments, so a follow-up sees what was said in the meantime. - **Reply.** Posts a comment on the issue or pull request. Knecht appends the preview link and, when one exists, the pull request link. - **Set labels.** Adds or removes labels of the repository. Knecht never creates labels, so the ones the agent should use have to exist before the run. - **Open a pull request.** Pushes the current branch and opens a pull request against the default branch, without the git steps above. The agent refuses to do this on the default branch itself. ### Mentions Comment on an issue or pull request of the project's repository: ```text @knecht-works fix the broken footer link ``` The name of your GitHub App works instead of `@knecht-works` as well. Knecht reacts with an eyes emoji, runs the comment as a follow-up, and posts the answer in the thread. Knecht only answers GitHub accounts that can sign in to your Knecht dashboard: the owner and everyone invited under Settings, Access, see [Inviting Your Team](https://knecht.works/docs/get-started/setup#inviting-your-team). Write access to the repository is not enough. Comments from all other accounts and from bots are ignored. The one-time setup, what happens step by step, and what to check when Knecht stays silent is on [Mentions](https://knecht.works/docs/usage/agent#mentions). ## Examples The workflows below can be built in the editor or imported from YAML under Workflows, "Import". Triggers are added in the editor after the import. ### Software Factory Three workflows that hand an issue from one to the next. The triage checks every new issue and sets the label `bug` or `enhancement`, and that label fires the trigger of the matching workflow. A bug comes back with a pull request, an enhancement with a plan that a mention in the thread turns into a pull request. All of it runs in the [session](https://knecht.works/#sessions-on-issues-and-pull-requests) of the issue. The triage boots the site once and every later run finds it running, so only the triage has a boot step. The prompts do not repeat title or body, because the agent reads the issue of its session itself. Both labels have to exist in the repository, `bug` and `enhancement` are GitHub's defaults. #### Triage | Source | Fires When | But Only If | | ------------- | ---------- | ------------- | | GitHub, Issue | Opened | No conditions | ::docs-workflow-yaml{filename="github-issue-triage.yaml"} ```yaml version: 1 name: GitHub Issue Triage description: Check every new issue and label it as bug or enhancement. steps: - type: ddev-start id: boot_project label: Boot the Site - type: ai prompt: 'Read the issue of this session. Try to reproduce it on the preview and in the code. Do not fix anything. Then apply exactly one label: "bug" if something is broken, "enhancement" if it was never built. If the report lacks the details to decide, apply no label and ask the reporter in the thread.' id: triage label: Triage ``` :: #### Bug Fix | Source | Fires When | But Only If | | ------------- | ------------------ | ------------- | | GitHub, Issue | Label added: `bug` | No conditions | ::docs-workflow-yaml{filename="github-bug-fix.yaml"} ```yaml version: 1 name: GitHub Bug Fix description: Fix an issue labeled as bug and open a pull request. steps: - type: ai prompt: The issue of this session was confirmed as a bug. Fix it on a work branch, verify the fix on the preview, commit, and open a pull request that closes the issue. Answer in the thread with a short summary of the change. id: fix label: Bug Fix ``` :: #### Add Enhancement | Source | Fires When | But Only If | | ------------- | -------------------------- | ------------- | | GitHub, Issue | Label added: `enhancement` | No conditions | ::docs-workflow-yaml{filename="github-add-enhancement.yaml"} ```yaml version: 1 name: GitHub Add Enhancement description: Plan an issue labeled as enhancement and post the plan. steps: - type: ai prompt: "The issue of this session is an enhancement. Work out how it would be built in this project: the files to touch, the steps in order, and what is open. Do not change any files. Post the plan in the thread, written for the person who opened the issue." id: plan label: Plan Enhancement ``` :: ::note A label that a person adds to an issue the triage never saw starts these workflows without a booted site. Add a boot step to them if that happens in your team. :: #### Result Someone reports that the site has no dark mode. A minute later the triage has labeled the issue as an enhancement. ![A new issue on GitHub that Knecht labeled as enhancement a minute after it was opened](https://knecht.works/assets/issue-creation-with-label.png) The label starts "GitHub Add Enhancement", and the plan lands in the thread, with the preview link Knecht appends. ![Knecht confirmed the issue as a feature request, added the enhancement label, and posted a plan with a preview link](https://knecht.works/assets/issue-follow-up.png) A team member agrees and mentions Knecht. It continues in the same session and answers with the pull request. ![The reporter asked Knecht to implement the plan and open a pull request, and Knecht answered with the pull request link and a summary](https://knecht.works/assets/issue-enhancement-finished.png) ### Pull Request Review Knecht tests every pull request on the running site. The run checks out the pull request's branch and the agent posts its findings as a comment. New commits fire the trigger again in the same session, so the second review knows the first. Do not exclude authors with `*[bot]` if Knecht's own pull requests should be reviewed: they are opened under the app's bot account. | Source | Fires When | But Only If | | -------------------- | -------------------------------------------- | --------------------------------------------------------------------------------------------------------- | | GitHub, Pull request | Opened, Ready for review, New commits pushed | Base branch is `main` and PR state is Ready for review and Author is not `renovate[bot], dependabot[bot]` | ::docs-workflow-yaml{filename="github-review-pr.yaml"} ```yaml version: 1 name: GitHub Pull Request Review description: Test each pull request on the preview and comment the findings. steps: - type: ddev-start id: boot_project label: Boot the Site - type: ai prompt: "Review the pull request of this session. Read the diff against {{ project.defaultBranch }}, then check the change on the preview. Post one comment: what the change does, what you tested, and anything that looks wrong. Do not change any files." id: review label: Review Pull Request ``` :: # Jira The Jira integration connects a Jira Cloud site to your instance. Tickets start workflows, the agent reads and answers on them, moves them through the board, and Knecht reports the finished pull request back on the ticket. ## Setup You need a Jira Cloud site and an account Knecht acts as. A dedicated account named "Knecht" is worth the extra seat: comments show up under its name, "assign it to Knecht" becomes a normal assignment in Jira, and the account's permissions limit what Knecht can see. Registering the webhook in step three needs Jira administrator rights, once. ::steps{level="3"} ### Create an API Token Sign in to Atlassian as the account Knecht should use and open [API tokens](https://id.atlassian.com/manage-profile/security/api-tokens){rel=""nofollow""}. "Create API token", give it a name such as "Knecht", and copy the token. It starts with `ATATT` and is shown only once. ### Connect the Account In your instance, open Settings, Integrations. The Jira panel has three fields: - **Site URL.** The Atlassian site, for example `https://acme.atlassian.net`. It has to start with `https://`. - **Email.** The email of the account from step one. - **API token.** The token from step one. "Connect" checks the credentials against Jira before anything is stored and shows "Connected as" with the account's name on success. The token is stored encrypted and not shown again. "Reconnect" replaces the token later, "Disconnect" removes the connection. ![The Jira panel under Settings, Integrations, connected to an account, with the site URL, email, and API token fields, and the webhook URL and secret below](https://knecht.works/assets/knecht-integration-settings-jira.png) ### Register the Webhook in Jira Jira does not call Knecht until you tell it to. After connecting, the panel shows "Waiting for Jira" with the webhook URL and a secret Knecht generated for you, both with copy buttons. "Set up in Jira" opens the webhook page of your site. By hand it is the gear icon, System, and "WebHooks" under Advanced in the sidebar. :::warning The button that adds a webhook sits at the far right of the WebHooks page and is often outside the visible area. If you only see the search field and the list, scroll the page horizontally all the way to the right. ::: Fill in the form: - **Name.** Anything, for example "Knecht". - **Status.** Enabled. - **URL.** The webhook URL from the panel, it ends in `/api/jira/webhook`. - **Secret.** Paste the secret from the panel. Do not use "Generate secret", Knecht has to know the value, and Jira does not show it again after saving. - **Issue related events.** In the column Issue tick "created", "updated", and "deleted". In the column Comment tick "created". Everything else stays empty. - **Exclude body.** Leave it unchecked. Knecht needs the ticket data in the delivery. ![The events section of the Jira webhook form, with created, updated, and deleted ticked under Issue and created ticked under Comment](https://knecht.works/assets/jira-webhook-events.png) The JQL field above the checkboxes is optional. Left empty, Jira warns that the query matches all issues in all projects. That works, Knecht drops events of projects that are not linked. A query such as `project in (KAN, WEB)` limits the deliveries to the projects you link in the next step. Atlassian describes the form under [Manage webhooks](https://support.atlassian.com/jira-cloud-administration/docs/manage-webhooks/){rel=""nofollow""}. Save the webhook at the bottom of the form. The summary should list the issue events created, updated, and deleted, the comment event created, and "Exclude body: No". ![The saved webhook in Jira, enabled, with the Knecht URL, the secret set, the issue and comment events, and exclude body set to no](https://knecht.works/assets/jira-webhook-created.png) ### Link a Project to Its Jira Project Open the settings of a project in Knecht. The Jira panel has one field, "Jira project", listing the projects the account can see. Pick the one whose tickets belong to this repository. One Jira project links to one repository and the other way round. Only linked projects can have Jira triggers. ![The Jira panel in the project settings, with the Jira project field set to a project](https://knecht.works/assets/knecht-project-settings-jira.png) ### Check That Events Arrive Edit any ticket in the linked Jira project. The status line in the Jira panel under Settings, Integrations switches to "Receiving events" with the last event and ticket key. If it does not: | Status line says | What to do | | ------------------------ | ----------------------------------------------------------------------------------------- | | Wrong secret | Paste the secret from the panel into the webhook again. | | Without a body | Uncheck "Exclude body" in the webhook. | | No project is linked | Link the Jira project in the project settings, see step four. | | Still "Waiting for Jira" | Check that the webhook URL is reachable from the internet and that the events are ticked. | :: ## Capabilities ### Triggers A Jira trigger watches the Jira projects linked to the selected projects and starts one run for the project whose Jira project the ticket belongs to. The project field only lists projects that are linked. In the form, "Fires when any of these happens" lists the events and "But only if" the conditions. Any ticked event fires the trigger, and one group of conditions has to hold. How the two sections work in general is on [Triggers](https://knecht.works/docs/usage/triggers#events-and-conditions). What all issue trackers share, such as issues that are created with the label already set, is on [Triggers](https://knecht.works/docs/usage/triggers#issue-trackers). ![The trigger dialog with Jira selected as source, two linked projects as chips, the events Assigned to Knecht and Label added with the label knecht ticked, and the conditions Issue type is Bug and Status is not Any Done](https://knecht.works/assets/knecht-trigger-jira.png) #### Events | Event | Fires when | Notes | | ------------------ | -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Created | A new ticket appears in the project. | | | Assigned to Knecht | The ticket is assigned to the account behind the connection. | Ticked by default. Makes "give it to Knecht" a normal assignment in Jira. Knecht taking the ticket itself does not fire, see [Triggers](https://knecht.works/docs/usage/triggers#knecht-holds-the-issue). | | Label added | One of the picked labels is added to a ticket. | The labels are picked from the labels of the Jira site. | | Status reached | A ticket moves into one of the picked statuses or status categories. | The default is "Any Done". A ticket created in a status does not fire. | #### Status Reached The status list has two groups. - **Category.** "Any To Do", "Any In Progress", and "Any Done" stand for Jira's three status categories, whatever the columns of a board are called. - **Exact status.** The statuses on the boards of the selected projects. [Triggers](https://knecht.works/docs/usage/triggers#issue-trackers) explains when a category and when an exact status fires, [Matching](https://knecht.works/docs/usage/triggers#matching) what happens with several projects. If the form warns about a missing status, pick a category, a status all projects share, or create a second trigger. ![The trigger dialog with two projects selected, Status reached set to In Planning, and a warning that this status does not exist in the Jira project KAN](https://knecht.works/assets/knecht-trigger-jira-status-error.png) #### Conditions | Field | Compares against | | ---------- | -------------------------------------------------------------------------------------------------- | | Status | The status the ticket is in, as a category such as "Any Done" or an exact status. | | Assignee | "Knecht", the account behind the connection. | | Issue type | The type of the ticket, for example Bug or Task. The types are read from the linked Jira projects. | | Label | The labels on the ticket, read from the Jira site. | Each condition takes "is" or "is not". Every value is picked from a list and compared exactly, upper and lower case included. The rules are the same for every source, see [Matching](https://knecht.works/docs/usage/triggers#matching). #### Inputs The ticket fills the run inputs: `identifier` is the key such as `PROJ-123`, `title` the summary, `body` the description converted to Markdown, `url` the ticket link, `status` the status name, `labels` the labels, `assignee` and `author` the display names of the assignee and reporter. `event` is `issue`. Pausing, versioning, and shared behavior are on [Triggers](https://knecht.works/docs/usage/triggers). ### Sessions on Tickets A run started by a ticket joins the [session](https://knecht.works/docs/get-started/concepts#sessions) of that ticket, like a run on a GitHub issue. The session keeps the checkout, the environment, and the agent conversation, so a second trigger on the same ticket or a mention continues the work. The session closes when the ticket reaches a status in the Done category or is deleted, its environment stops once no run needs it any more, and it opens again when the ticket is moved back. ### The Agent on Tickets When the run's session belongs to a ticket, the [agent](https://knecht.works/docs/usage/agent) gets four commands inside the environment. The prompt of an AI step only has to name what to do with them. - **Read the ticket.** Type, status, reporter, assignee, labels, the description, and the last ten comments, live from Jira. - **Reply.** Posts a comment on the ticket. The agent writes Markdown, Knecht converts it to Jira's format and appends the preview link and, when one exists, the pull request link. An `@Name` of the reporter, the assignee, or a commenter becomes a real mention that notifies that person. - **Set labels.** Adds or removes labels of the Jira site. Knecht never creates labels. If a name does not exist, the agent gets the list of existing labels instead. - **Move the ticket.** Transitions the ticket to a status by name, for example "In Review". If no transition from the current status leads there, the agent gets the list of reachable statuses instead. Opening a pull request works through the git steps or the agent's own command, see [GitHub](https://knecht.works/docs/integrations/github#workflow-steps). ### Mentions Write a comment on a ticket of a linked Jira project, mention the Knecht account the way you mention a colleague, and add the instruction: ```text @Knecht fix the broken footer link ``` `@knecht` typed as plain text works as well. Knecht runs the comment as a follow-up and posts the answer as a comment. Whoever can comment on the ticket can mention Knecht, the Jira site is the gate. The one-time setup, what happens step by step, and what to check when Knecht stays silent is on [Mentions](https://knecht.works/docs/usage/agent#mentions). ## Examples The workflows below can be built in the editor or imported from YAML under Workflows, "Import". Triggers are added in the editor after the import. All of them assume the project is linked to its Jira project. ### Software Factory Three workflows that hand a ticket from one to the next. The triage checks every new ticket, sets the label `bug` or `enhancement`, and moves it to "To Do". That move fires the trigger of the workflow whose label condition matches. A bug comes back with a pull request and a ticket in review, an enhancement with a plan that a mention in the comments turns into a pull request. All of it runs in the [session](https://knecht.works/#sessions-on-tickets) of the ticket. The triage boots the site once and every later run finds it running, so only the triage has a boot step. The prompts do not repeat summary or description, because the agent reads the ticket of its session itself. ::note Make sure the labels `bug` and `enhancement` exist in Jira first. Jira has no label settings, a label exists once any ticket carries it. Knecht only applies labels that exist, and a trigger only offers labels that exist. :: #### Triage | Source | Fires When | But Only If | | ------ | ---------- | ------------- | | Jira | Created | No conditions | ::docs-workflow-yaml{filename="jira-issue-triage.yaml"} ```yaml version: 1 name: Jira Issue Triage description: Check every new ticket and label it as bug or enhancement. steps: - type: ddev-start id: boot_project - type: ai prompt: >- Read the ticket of this session. Try to reproduce it in the preview and in the code. Do not fix anything. Then apply exactly one label: "bug" if something is broken, "enhancement" if it was never built and then move it to "To Do". If the ticket lacks the details to decide, apply no label and ask the reporter in a comment for clarification. id: triage label: Triage ``` :: #### Bug Fix | Source | Fires When | But Only If | | ------ | ----------------------- | -------------- | | Jira | Status reached: `To Do` | Label is `bug` | ::docs-workflow-yaml{filename="jira-bug-fix.yaml"} ```yaml version: 1 name: Jira Bug Fix description: Fix a ticket labeled as bug and open a pull request. steps: - type: ai prompt: >- The ticket of this session was confirmed as a bug. When you are beginning your work move the Ticket to "In Progress". Then fix it on a work branch, verify the fix on the preview, commit in logical batches, and open a pull request. Comment a short summary of the change on the ticket and move it to "In Review". id: bug_fix label: Bug Fix ``` :: #### Add Enhancement | Source | Fires When | But Only If | | ------ | ----------------------- | ---------------------- | | Jira | Status reached: `To Do` | Label is `enhancement` | ::docs-workflow-yaml{filename="jira-add-enhancement.yaml"} ```yaml version: 1 name: Jira Add Enhancement description: Plan a ticket labeled as enhancement and post the plan. steps: - type: ai prompt: >- The ticket of this session is an enhancement. Work out how it would be built in this project: the files to touch, the steps in order, and what is open. Do not change any files or the status. Just post the plan as a comment, written for the person who reported the ticket. id: plan_enhancement label: Plan Enhancement ``` :: When the team agrees with the plan, a comment that mentions the Knecht account, such as "implement it and open a PR", is enough. Knecht continues in the same session and answers with the pull request. ::note A ticket that a person labels and moves to "To Do" without the triage having seen it starts these workflows without a booted site. Add a boot step to them if that happens in your team. :: ### Assign to Knecht No label and no triage: the team assigns a ticket to the Knecht account like to any colleague, and Knecht implements it. This workflow stands on its own, so it boots the site itself. | Source | Fires When | But Only If | | ------ | ------------------ | ------------------------- | | Jira | Assigned to Knecht | Issue type is `Bug, Task` | ::docs-workflow-yaml{filename="jira-assigned-ticket.yaml"} ```yaml version: 1 name: Jira Assigned Ticket description: Implement a ticket that was assigned to Knecht. steps: - type: ddev-start id: boot_project label: Boot the Site - type: ai prompt: The ticket of this session was assigned to you. Read it with its comments, implement it on a work branch, verify it on the preview, commit, and open a pull request. Comment a short summary on the ticket and move it to "In Review". id: implement label: Implement ``` :: ### Estimate Tickets A ticket that is moved to "In Planning" gets an estimate and lands in "To Do". The agent reads the ticket, looks up the code it touches, and comments how big the work is and why. An estimate needs the code, not the running site, so there is no boot step. Pick the exact status of your board in the trigger, the names here are examples. | Source | Fires When | But Only If | | ------ | --------------------------- | ------------- | | Jira | Status reached: In Planning | No conditions | ::docs-workflow-yaml{filename="jira-estimate-ticket.yaml"} ```yaml version: 1 name: Jira Estimate Ticket description: Estimate a ticket in planning and move it to To Do. steps: - type: ai prompt: 'Estimate the ticket of this session. Find the code it touches and judge the effort. Do not change any files. Comment the estimate on the ticket: a size (S, M, L, or XL), the hours you expect, what drives the effort, and what is unclear. Then move the ticket to "To Do". If it is too vague to estimate, comment your questions and leave it where it is.' id: estimate label: Estimate ``` :: # Plane The Plane integration connects a Plane workspace to your instance, on Plane Cloud or self-hosted. Work items start workflows, the agent reads and answers on them, moves them between states, and Knecht reports the finished pull request back on the work item. ## Setup You need a Plane workspace and an account Knecht acts as. A dedicated account named "Knecht" is worth the extra seat: comments show up under its name, "assign it to Knecht" becomes a normal assignment in Plane, and the account's project memberships limit what Knecht can see. Creating the webhook in step three needs workspace admin rights, once. ::steps{level="3"} ### Create a Personal Access Token Sign in to Plane as the account Knecht should use and open the profile settings. Under Developer, "Personal Access Tokens", click "Add access token". Give it a title such as "Knecht", leave "Never expires" on or set an expiration date, and click "Generate token". Copy the token, it starts with `plane_api_` and is shown only once. ![The Create token dialog in the Plane profile settings, with the title Knecht and Never expires switched on](https://knecht.works/assets/plane-access-token.png) A token with an expiration date stops working on that day. Create a new one and use "Reconnect" in Knecht before it runs out. ### Connect the Account In your instance, open Settings, Integrations. The Plane panel has three fields: - **Plane URL.** `https://app.plane.so` for Plane Cloud, or the address of your own Plane. It has to start with `https://`. - **Workspace slug.** The first path segment of your Plane URL. For `https://app.plane.so/acme/projects` it is `acme`. - **API key.** The token from step one. "Connect" checks the credentials against Plane before anything is stored and shows "Connected as" with the account's name on success. The token is stored encrypted and not shown again. "Reconnect" replaces the token later, "Disconnect" removes the connection. ![The Plane panel under Settings, Integrations, connected to an account, with the Plane URL, workspace slug, and API key fields, and the webhook URL and secret below](https://knecht.works/assets/knecht-integration-settings-plane.png) ### Create the Webhook in Plane Plane does not call Knecht until you tell it to. After connecting, the panel shows "Create the webhook in Plane and paste its secret below" with the webhook URL and a copy button. "Set up in Plane" opens the webhook page of your workspace. By hand it is Workspace settings, then "Webhooks" under Developers. Fill in the form: - **Webhook title.** Anything, for example "Knecht". - **Payload URL.** The webhook URL from the panel, it ends in `/api/plane/webhook`. - **V1 scopes.** Leave everything unticked, they are deprecated. - **V2 scopes.** Tick "Work items" and "Work item comments". Everything else stays empty. - **Work item v2 filters.** Leave it empty. Knecht drops events of projects that are not linked. ![The Create webhook form in the Plane workspace settings, with the Knecht payload URL and the V2 scopes Work items and Work item comments ticked](https://knecht.works/assets/plane-webhook-events.png) Knecht uses the work item events created, updated, archived, and deleted, and the comment event created. Updated and deleted comments arrive as well and are ignored. "Create" saves the webhook, and Plane generates a secret key for it. Copy the secret key, paste it into the Secret field of the Plane panel in Knecht, and click "Save". The pencil next to the secret replaces it later, for example after regenerating the key in Plane. The status line switches to "Waiting for Plane". ### Link a Project to Its Plane Project Open the settings of a project in Knecht. The Plane panel has one field, "Plane project", listing the projects the account is a member of. Pick the one whose work items belong to this repository. One Plane project links to one repository and the other way round. Only linked projects can have Plane triggers. ### Check That Events Arrive Edit any work item in the linked Plane project. The status line in the Plane panel under Settings, Integrations switches to "Receiving events" with the last event and work item key. If it does not: | Status line says | What to do | | ------------------------- | --------------------------------------------------------------------------------------------------------------- | | Wrong secret | Replace the secret in the panel with the secret key of the webhook. | | Without a body | Create the webhook again, Plane sent an empty delivery. | | No project is linked | Link the Plane project in the project settings, see step four. Events Knecht does not use show up here as well. | | Still "Waiting for Plane" | Check that the webhook URL is reachable from the internet and that the scopes are ticked. | :: ## Capabilities ### Triggers A Plane trigger watches the Plane projects that are linked to the selected projects. When a work item fires it, one run starts in the project that work item's Plane project is linked to. The project field only lists projects that are linked. In the form, "Fires when any of these happens" lists the events and "But only if" the conditions. Any ticked event fires the trigger, and one group of conditions has to hold. How the two sections work in general is on [Triggers](https://knecht.works/docs/usage/triggers#events-and-conditions). What all issue trackers share, such as issues that are created with the label already set, is on [Triggers](https://knecht.works/docs/usage/triggers#issue-trackers). ![The trigger dialog with Plane selected as source, one linked project, the event Assigned to Knecht ticked, and the conditions Label is Bug and Priority is urgent](https://knecht.works/assets/knecht-trigger-plane.png) #### Events | Event | Fires when | Notes | | ------------------ | ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Created | A new work item appears in the project. | | | Assigned to Knecht | The work item is assigned to the account behind the connection. | Ticked by default. Makes "give it to Knecht" a normal assignment in Plane. Knecht [joining the assignees](https://knecht.works/#knecht-holds-the-work-item) itself does not fire. | | Label added | One of the picked labels is added to a work item. | The labels are picked from the labels of the Plane project. | | State reached | A work item moves into one of the picked states or state groups. | The default is "Any Completed". A work item created in a state does not fire. | #### State Reached The state list has two groups. - **Group.** "Any Backlog", "Any Unstarted", "Any Started", "Any Completed", and "Any Cancelled" stand for Plane's five state groups, whatever the states of a project are called. - **Exact state.** The states of the selected projects. [Triggers](https://knecht.works/docs/usage/triggers#issue-trackers) explains when a group and when an exact state fires, [Matching](https://knecht.works/docs/usage/triggers#matching) what happens with several projects. If the form warns about a missing state, pick a group, a state all projects share, or create a second trigger. #### Conditions | Field | Compares against | | -------- | ---------------------------------------------------------------------------------- | | State | The state the work item is in, as a group such as "Any Started" or an exact state. | | Assignee | "Knecht", the account behind the connection. | | Label | The labels on the work item, read from the Plane project. | | Priority | The priority of the work item: `urgent`, `high`, `medium`, `low`, or `none`. | Each condition takes "is" or "is not". Every value is picked from a list and compared exactly, upper and lower case included. The rules are the same for every source, see [Matching](https://knecht.works/docs/usage/triggers#matching). #### Inputs The work item fills the run inputs: `identifier` is the key such as `WEB-42`, `title` the name, `body` the description converted to Markdown, `url` the work item link, `status` the state name, `labels` the labels, `assignee` and `author` the names of the assignees and the creator. `event` is `issue`. Pausing, versioning, and shared behavior are on [Triggers](https://knecht.works/docs/usage/triggers). ### Sessions on Work Items A run started by a work item joins the [session](https://knecht.works/docs/get-started/concepts#sessions) of that work item, like a run on a GitHub issue. The session keeps the checkout, the environment, and the agent conversation, so a second trigger on the same work item or a mention continues the work. The session closes when the work item reaches a state in the Completed or Cancelled group, is archived, or is deleted, its environment stops once no run needs it any more, and it opens again when the work item is moved back. ### Knecht Holds the Work Item Knecht joins the assignees of a work item while it works on it, as on every issue tracker, see [Triggers](https://knecht.works/docs/usage/triggers#knecht-holds-the-issue). A work item takes several assignees, so the people already on it stay on it, and when Knecht leaves, they keep the work item. If Knecht was the only assignee, the work item goes to the person whose change started the run, else to the creator, else it is left unassigned. ### The Agent on Work Items When the run's session belongs to a work item, the [agent](https://knecht.works/docs/usage/agent) gets four commands inside the environment. The prompt of an AI step only has to name what to do with them. - **Read the work item.** State, creator, assignees, labels, the description, and the last ten comments, live from Plane. - **Reply.** Posts a comment on the work item. The agent writes Markdown, Knecht converts it to Plane's format and appends the preview link and, when one exists, the pull request link. An `@Name` of a project member becomes a real mention that notifies that person. - **Set labels.** Adds or removes labels of the Plane project. Knecht never creates labels. If a name does not exist, the agent gets the list of existing labels instead. - **Move the work item.** Sets the state by name, for example "In Review". Plane has no transition rules, so every state of the project is reachable. Opening a pull request works through the git steps or the agent's own command, see [GitHub](https://knecht.works/docs/integrations/github#workflow-steps). ### Mentions Write a comment on a work item of a linked Plane project, mention the Knecht account the way you mention a colleague, and add the instruction: ```text @Knecht fix the broken footer link ``` `@knecht` typed as plain text works as well. Knecht runs the comment as a follow-up and posts the answer as a comment. Whoever can comment on the work item can mention Knecht, the Plane workspace is the gate. The one-time setup, what happens step by step, and what to check when Knecht stays silent is on [Mentions](https://knecht.works/docs/usage/agent#mentions). ## Examples The workflows below can be built in the editor or imported from YAML under Workflows, "Import". Triggers are added in the editor after the import. All of them assume the project is linked to its Plane project. ### Software Factory Three workflows that hand a work item from one to the next. The triage checks every new work item, sets the label `Bug` or `Enhancement`, and moves it to "Todo". That move fires the trigger of the workflow whose label condition matches. A bug comes back with a pull request and a work item in review, an enhancement with a plan that a mention in the comments turns into a pull request. All of it runs in the [session](https://knecht.works/#sessions-on-work-items) of the work item. The triage boots the site once and every later run finds it running, so only the triage has a boot step. The prompts do not repeat name or description, because the agent reads the work item of its session itself. ::note Plane creates the label `Bug` with every project, add `Enhancement` under the project settings, "Labels", first. Knecht only applies labels that exist, and a trigger only offers labels that exist. The state "In Review" is not a default either: add it under "States" in the Started group, a state in the Completed group would close the session. :: #### Triage | Source | Fires When | But Only If | | ------ | ---------- | ------------- | | Plane | Created | No conditions | ::docs-workflow-yaml{filename="plane-work-item-triage.yaml"} ```yaml version: 1 name: Plane Work Item Triage description: Check every new work item and label it as bug or enhancement. steps: - type: ddev-start id: boot_project - type: ai prompt: >- Read the work item of this session. Try to reproduce it in the preview and in the code. Do not fix anything. Then apply exactly one label: "Bug" if something is broken, "Enhancement" if it was never built and then move it to "Todo". If the work item lacks the details to decide, apply no label and ask the creator in a comment for clarification. id: triage label: Triage ``` :: #### Bug Fix | Source | Fires When | But Only If | | ------ | --------------------- | -------------- | | Plane | State reached: `Todo` | Label is `Bug` | ::docs-workflow-yaml{filename="plane-bug-fix.yaml"} ```yaml version: 1 name: Plane Bug Fix description: Fix a work item labeled as bug and open a pull request. steps: - type: ai prompt: >- The work item of this session was confirmed as a bug. When you are beginning your work move the work item to "In Progress". Then fix it on a work branch, verify the fix on the preview, commit in logical batches, and open a pull request. Comment a short summary of the change on the work item and move it to "In Review". id: bug_fix label: Bug Fix ``` :: #### Add Enhancement | Source | Fires When | But Only If | | ------ | --------------------- | ---------------------- | | Plane | State reached: `Todo` | Label is `Enhancement` | ::docs-workflow-yaml{filename="plane-add-enhancement.yaml"} ```yaml version: 1 name: Plane Add Enhancement description: Plan a work item labeled as enhancement and post the plan. steps: - type: ai prompt: >- The work item of this session is an enhancement. Work out how it would be built in this project: the files to touch, the steps in order, and what is open. Do not change any files or the state. Just post the plan as a comment, written for the person who created the work item. id: plan_enhancement label: Plan Enhancement ``` :: When the team agrees with the plan, a comment that mentions the Knecht account, such as "implement it and open a PR", is enough. Knecht continues in the same session and answers with the pull request. ::note A work item that a person labels and moves to "Todo" without the triage having seen it starts these workflows without a booted site. Add a boot step to them if that happens in your team. :: ### Urgent Bugs to Knecht No triage: the team assigns a work item to the Knecht account like to any colleague, and Knecht implements it. The conditions keep it to urgent bugs, everything else assigned to Knecht stays untouched. This workflow stands on its own, so it boots the site itself. | Source | Fires When | But Only If | | ------ | ------------------ | --------------------------------------- | | Plane | Assigned to Knecht | Label is `Bug` and Priority is `urgent` | ::docs-workflow-yaml{filename="plane-assigned-work-item.yaml"} ```yaml version: 1 name: Plane Assigned Work Item description: Implement a work item that was assigned to Knecht. steps: - type: ddev-start id: boot_project label: Boot the Site - type: ai prompt: The work item of this session was assigned to you. Read it with its comments, implement it on a work branch, verify it on the preview, commit, and open a pull request. Comment a short summary on the work item and move it to "In Review". id: implement label: Implement ``` :: ### Estimate Work Items A work item that is moved to "In Planning" gets an estimate and lands in "Todo". The agent reads the work item, looks up the code it touches, and comments how big the work is and why. An estimate needs the code, not the running site, so there is no boot step. Pick the exact state of your project in the trigger, the names here are examples. | Source | Fires When | But Only If | | ------ | -------------------------- | ------------- | | Plane | State reached: In Planning | No conditions | ::docs-workflow-yaml{filename="plane-estimate-work-item.yaml"} ```yaml version: 1 name: Plane Estimate Work Item description: Estimate a work item in planning and move it to Todo. steps: - type: ai prompt: 'Estimate the work item of this session. Find the code it touches and judge the effort. Do not change any files. Comment the estimate on the work item: a size (S, M, L, or XL), the hours you expect, what drives the effort, and what is unclear. Then move the work item to "Todo". If it is too vague to estimate, comment your questions and leave it where it is.' id: estimate label: Estimate ``` :: # Linear The Linear integration connects a Linear workspace to your instance. Issues start workflows, the agent reads and answers on them, moves them between statuses, and Knecht reports the finished pull request back on the issue. ## Setup You need a Linear workspace and an account Knecht acts as. A dedicated account named "Knecht" is worth the extra seat: comments show up under its name, "assign it to Knecht" becomes a normal assignment in Linear, and the account's team memberships limit what Knecht can see. Creating the webhook in step three needs workspace admin rights, once. ::steps{level="3"} ### Create a Personal API Key Sign in to Linear as the account Knecht should use and open Settings, "Security & access". Under "Personal API keys", create a new key. Give it a name such as "Knecht" and fill in the form: - **Permissions.** "Full access", or under "Only select permissions" at least Read and Write. Knecht reads issues and comments, writes comments, and changes labels and statuses. - **Team access.** "All teams you have access to", or under "Only select teams" the teams you want to link to projects. ![The Create API key form in the Linear security settings, with the key name Knecht, Full access, and All teams you have access to selected](https://knecht.works/assets/linear-api-key.png) "Create" shows the key. Copy it, it starts with `lin_api_` and is shown only once. Everything Knecht does with the key is attributed to this account. ### Connect the Account In your instance, open Settings, Integrations. The Linear panel has one field, "API key", for the key from step one. "Connect" checks the key against Linear before anything is stored and shows "Connected as" with the account's name on success. The key is stored encrypted and not shown again. "Reconnect" replaces the key later, "Disconnect" removes the connection. ![The Linear panel under Settings, Integrations, connected to an account, with the API key field, and the webhook URL and secret below](https://knecht.works/assets/knecht-integration-settings-linear.png) ### Create the Webhook in Linear Linear does not call Knecht until you tell it to. After connecting, the panel shows the webhook URL with a copy button. "Set up in Linear" opens the API settings of your workspace. By hand it is Settings, then "API" under Administration, then "Webhooks". Fill in the form: - **Label.** Anything, for example "Knecht". - **URL.** The webhook URL from the panel, it ends in `/api/linear/webhook`. - **Data change events.** Tick "Issues" and "Comments". Everything else stays empty. - **Other events.** Leave "Issue SLA" unticked. - **Team selection.** "All public teams", or the one team you link. It cannot be changed after creation. Knecht drops events of teams that are not linked. ![The Create webhook form in the Linear API settings, with the Knecht webhook URL and the data change events Comments and Issues ticked](https://knecht.works/assets/linear-webhook-events.png) Knecht uses the issue events create, update, and remove, and the comment event create. Updated and removed comments arrive as well and are ignored. The form already shows the signing secret, it starts with `lin_wh_`. Copy it, click "Create webhook", paste the secret into the Secret field of the Linear panel in Knecht, and click "Save". Unlike Jira, the secret comes from Linear, not from Knecht. Knecht keeps one signing secret, so use one webhook for all linked teams. The pencil next to the secret replaces it later. The status line switches to "Waiting for Linear". ### Link a Project to Its Linear Team Open the settings of a project in Knecht. The Linear panel has one field, "Linear team", listing the teams the account can access. Pick the one whose issues belong to this repository. One Linear team links to one repository and the other way round. Only linked projects can have Linear triggers. ### Check That Events Arrive Edit any issue of the linked team. The status line in the Linear panel under Settings, Integrations switches to "Receiving events" with the last event and issue identifier. If it does not: | Status line says | What to do | | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | Wrong secret | Replace the secret in the panel with the signing secret of the webhook. | | Without a body | Create the webhook again, Linear sent an empty delivery. | | No project is linked | Link the Linear team in the project settings, see step four. Events Knecht does not use show up here as well. | | Still "Waiting for Linear" | Check that the webhook URL is reachable from the internet, that the events are ticked, and that the team selection covers the linked team. | :: ## Capabilities ### Triggers A Linear trigger watches the Linear teams linked to the selected projects and starts one run for the project whose team the issue belongs to. The project field only lists projects that are linked. In the form, "Fires when any of these happens" lists the events and "But only if" the conditions. Any ticked event fires the trigger, and one group of conditions has to hold. How the two sections work in general is on [Triggers](https://knecht.works/docs/usage/triggers#events-and-conditions). What all issue trackers share, such as issues that are created with the label already set, is on [Triggers](https://knecht.works/docs/usage/triggers#issue-trackers). ![The trigger dialog with Linear selected as source, one linked project, the event Created ticked, and the condition Status is Any Triage](https://knecht.works/assets/knecht-trigger-linear.png) #### Events | Event | Fires when | Notes | | ------------------ | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Created | A new issue appears in the team. | | | Assigned to Knecht | The issue is assigned to the account behind the connection. | Ticked by default. Makes "give it to Knecht" a normal assignment in Linear. Knecht taking the issue itself does not fire, see [Triggers](https://knecht.works/docs/usage/triggers#knecht-holds-the-issue). | | Label added | One of the picked labels is added to an issue. | The labels are picked from the labels of the team and the workspace. Label groups are not listed, they cannot be applied themselves. | | Status reached | An issue moves into one of the picked statuses or status categories. | The default is "Any Completed". An issue created in a status does not fire. | #### Status Reached The status list has two groups. - **Category.** "Any Triage", "Any Backlog", "Any Unstarted", "Any Started", "Any Completed", and "Any Canceled" stand for Linear's six status categories, whatever the statuses of a team are called. - **Exact status.** The statuses of the selected teams. [Triggers](https://knecht.works/docs/usage/triggers#issue-trackers) explains when a category and when an exact status fires, [Matching](https://knecht.works/docs/usage/triggers#matching) what happens with several projects. If the form warns about a missing status, pick a category, a status all teams share, or create a second trigger. #### Conditions | Field | Compares against | | -------- | ---------------------------------------------------------------------------------- | | Status | The status the issue is in, as a category such as "Any Triage" or an exact status. | | Assignee | "Knecht", the account behind the connection. | | Label | The labels on the issue, read from the team and the workspace. | | Priority | The priority of the issue: `urgent`, `high`, `medium`, `low`, or `none`. | Each condition takes "is" or "is not". Every value is picked from a list and compared exactly, upper and lower case included. The rules are the same for every source, see [Matching](https://knecht.works/docs/usage/triggers#matching). #### Inputs The issue fills the run inputs: `identifier` is the key such as `ENG-42`, `title` the title, `body` the description as Markdown, `url` the issue link, `status` the status name, `labels` the labels, `assignee` and `author` the names of the assignee and the creator. `event` is `issue`. Pausing, versioning, and shared behavior are on [Triggers](https://knecht.works/docs/usage/triggers). ### Sessions on Issues A run started by an issue joins the [session](https://knecht.works/docs/get-started/concepts#sessions) of that issue, like a run on a GitHub issue. The session keeps the checkout, the environment, and the agent conversation, so a second trigger on the same issue or a mention continues the work. The session closes when the issue reaches a status in the Completed or Canceled category, is archived, or is deleted, its environment stops once no run needs it any more, and it opens again when the issue is moved back. ### The Agent on Issues When the run's session belongs to an issue, the [agent](https://knecht.works/docs/usage/agent) gets four commands inside the environment. The prompt of an AI step only has to name what to do with them. - **Read the issue.** Priority, status, creator, assignee, labels, the description, and the last ten comments, live from Linear. - **Reply.** Posts a comment on the issue. The agent writes Markdown, which Linear takes as it is, and Knecht appends the preview link and, when one exists, the pull request link. - **Set labels.** Adds or removes labels of the team and the workspace. Knecht never creates labels. If a name does not exist, the agent gets the list of existing labels instead. - **Move the issue.** Sets the status by name, for example "In Review". Linear has no transition rules, so every status of the team is reachable. Opening a pull request works through the git steps or the agent's own command, see [GitHub](https://knecht.works/docs/integrations/github#workflow-steps). ### Mentions Write a comment on an issue of a linked Linear team, mention the Knecht account the way you mention a colleague, and add the instruction: ```text @Knecht fix the broken footer link ``` `@knecht` typed as plain text works as well. Knecht runs the comment as a follow-up and posts the answer as a comment. Whoever can comment on the issue can mention Knecht, the Linear workspace is the gate. The one-time setup, what happens step by step, and what to check when Knecht stays silent is on [Mentions](https://knecht.works/docs/usage/agent#mentions). ## Examples The workflows below can be built in the editor or imported from YAML under Workflows, "Import". Triggers are added in the editor after the import. All of them assume the project is linked to its Linear team. ### Software Factory Three workflows that hand an issue from one to the next. The triage checks every new issue, sets the label `Bug` or `Feature`, and moves it to "Todo". That move fires the trigger of the workflow whose label condition matches. A bug comes back with a pull request and an issue in review, a feature with a plan that a mention in the comments turns into a pull request. All of it runs in the [session](https://knecht.works/#sessions-on-issues) of the issue. The triage boots the site once and every later run finds it running, so only the triage has a boot step. The prompts do not repeat title or description, because the agent reads the issue of its session itself. ::note Make sure the labels `Bug` and `Feature` exist in the team or the workspace first. Knecht only applies labels that exist, and a trigger only offers labels that exist. :: #### Triage | Source | Fires When | But Only If | | ------ | ---------- | ------------- | | Linear | Created | No conditions | ::docs-workflow-yaml{filename="linear-issue-triage.yaml"} ```yaml version: 1 name: Linear Issue Triage description: Check every new issue and label it as bug or feature. steps: - type: ddev-start id: boot_project - type: ai prompt: >- Read the issue of this session. Try to reproduce it in the preview and in the code. Do not fix anything. Then apply exactly one label: "Bug" if something is broken, "Feature" if it was never built and then move it to "Todo". If the issue lacks the details to decide, apply no label and ask the creator in a comment for clarification. id: triage label: Triage ``` :: #### Bug Fix | Source | Fires When | But Only If | | ------ | ---------------------- | -------------- | | Linear | Status reached: `Todo` | Label is `Bug` | ::docs-workflow-yaml{filename="linear-bug-fix.yaml"} ```yaml version: 1 name: Linear Bug Fix description: Fix an issue labeled as bug and open a pull request. steps: - type: ai prompt: >- The issue of this session was confirmed as a bug. When you are beginning your work move the issue to "In Progress". Then fix it on a work branch, verify the fix on the preview, commit in logical batches, and open a pull request. Comment a short summary of the change on the issue and move it to "In Review". id: bug_fix label: Bug Fix ``` :: #### Plan Feature | Source | Fires When | But Only If | | ------ | ---------------------- | ------------------ | | Linear | Status reached: `Todo` | Label is `Feature` | ::docs-workflow-yaml{filename="linear-plan-feature.yaml"} ```yaml version: 1 name: Linear Plan Feature description: Plan an issue labeled as feature and post the plan. steps: - type: ai prompt: >- The issue of this session is a feature. Work out how it would be built in this project: the files to touch, the steps in order, and what is open. Do not change any files or the status. Just post the plan as a comment, written for the person who created the issue. id: plan_feature label: Plan Feature ``` :: When the team agrees with the plan, a comment that mentions the Knecht account, such as "implement it and open a PR", is enough. Knecht continues in the same session and answers with the pull request. ::note An issue that a person labels and moves to "Todo" without the triage having seen it starts these workflows without a booted site. Add a boot step to them if that happens in your team. :: ### Urgent Bugs to Knecht No triage: the team assigns an issue to the Knecht account like to any colleague, and Knecht implements it. The conditions keep it to urgent bugs, everything else assigned to Knecht stays untouched. This workflow stands on its own, so it boots the site itself. | Source | Fires When | But Only If | | ------ | ------------------ | --------------------------------------- | | Linear | Assigned to Knecht | Label is `Bug` and Priority is `urgent` | ::docs-workflow-yaml{filename="linear-assigned-issue.yaml"} ```yaml version: 1 name: Linear Assigned Issue description: Implement an issue that was assigned to Knecht. steps: - type: ddev-start id: boot_project label: Boot the Site - type: ai prompt: The issue of this session was assigned to you. Read it with its comments, implement it on a work branch, verify it on the preview, commit, and open a pull request. Comment a short summary on the issue and move it to "In Review". id: implement label: Implement ``` :: ### Estimate the Triage Inbox With Triage switched on for a team, issues from other tools and from people outside the team land in the Triage status first. This workflow gives each of them an estimate and moves it to "Backlog". A new issue is created in Triage and does not move there, so the trigger fires on "Created" with a condition on the status instead of on "Status reached". The agent reads the issue, looks up the code it touches, and comments how big the work is and why. An estimate needs the code, not the running site, so there is no boot step. | Source | Fires When | But Only If | | ------ | ---------- | -------------------- | | Linear | Created | Status is Any Triage | ::docs-workflow-yaml{filename="linear-estimate-issue.yaml"} ```yaml version: 1 name: Linear Estimate Issue description: Estimate an issue in triage and move it to Backlog. steps: - type: ai prompt: 'Estimate the issue of this session. Find the code it touches and judge the effort. Do not change any files. Comment the estimate on the issue: a size (S, M, L, or XL), the hours you expect, what drives the effort, and what is unclear. Then move the issue to "Backlog". If it is too vague to estimate, comment your questions and leave it where it is.' id: estimate label: Estimate ``` :: # Beta Testers Knecht is in a closed beta. This page covers what happens after you've applied: getting an instance, setting it up, and redeeming your OpenCode credit. ## Managed or Self-Hosted Before anything else, decide where your instance runs. Both options are the same software with the same setup flow, GitHub App, and updates. The difference is who operates the server and where your data lives. | | Managed | Self-hosted | | --------------- | ---------------------------------------------------- | ------------------------------------------------------- | | Server | A machine we operate | Your own, a fresh Ubuntu 24.04 VPS or a Mac | | Getting started | You receive a URL from us | You run the install script, a few minutes | | Updates | We apply them | You do, by a button on the System page or on a schedule | | Your data | Repositories, env files, and dumps sit on our server | Everything stays on your infrastructure | | Availability | Limited number of instances during the beta | Unlimited | Pick managed if you want to test Knecht without touching a server. Pick self-hosted if your projects' data must not leave your infrastructure, or if you want to evaluate the install and update experience as well. [Installation](https://knecht.works/docs/get-started/installation) lists the server requirements, [Maintenance](https://knecht.works/docs/get-started/maintenance) what running it involves. Tell us which one you want in the mail after your application. ## How Onboarding Works ::steps{level="3"} ### Get an Instance With managed hosting, you receive the URL of your instance from us and skip ahead. For a self-hosted instance, follow [Installation](https://knecht.works/docs/get-started/installation) on a fresh server. Both end at the same point: the dashboard is reachable under its domain. ### Set up the Instance The first visit creates the GitHub App and signs you in. [Setup](https://knecht.works/docs/get-started/setup) walks through it, including inviting your team. ### Add the First Project Connect a repository, paste its env, upload a database dump, and boot it. [Projects](https://knecht.works/docs/usage/projects) covers the guided setup and what the project page offers afterward. :: ## Getting Your OpenCode Credit The credit lives in an OpenCode workspace we manage. You do not need an OpenCode account of your own: ::steps{level="3"} ### Receive the Key We create a key for you in our workspace and send it to you. It starts with `oc_sk_` and is capped at your credit. ### Enter the Key in Knecht In your instance, open Settings > Agent. Pick "OpenCode Zen" as the provider, paste the key, and choose a default model from the list. :: ::warning Pick "OpenCode Zen", not "OpenCode Go". The credit is on Zen, and a Go model fails with this key. :: ::note Keys from the old OpenCode console, starting with `sk-`, no longer work. If your instance still uses one, write to and we send you a new key. :: # Troubleshooting Most projects boot on Knecht exactly as they do with [DDEV](https://ddev.com){rel=""nofollow""} locally. If yours doesn't, check the cases below, the exceptions we have met so far. Almost all of them share one cause: Knecht routes previews by hostname on one port, while the DDEV router convention puts a dev server on the same host and another port. A Vite dev server on Knecht therefore has its own origin, handed to the project as `KNECHT_DEV_SERVER_URL`. Any config that builds a "same host, other port" address needs to read that variable instead. ::note All fixes on this page are on the project side and leave local development unchanged. `KNECHT_DEV_SERVER_URL` is simply unset on your machine, so every fallback keeps working as before. After changing a Vite config, restart the session's dev server so the marker files are rewritten. :: ## Laravel ### Vite Origin from DDEV\_PRIMARY\_URL **Symptom.** The preview loads, but `/@vite/client`, `app.css`, and `app.js` fail. The tags point at the preview host on port 5173. **Cause.** `vite.config.js` builds `server.origin` from `DDEV_PRIMARY_URL` plus `:5173`. The Laravel Vite plugin writes that origin into `public/hot`, and Blade emits it verbatim. No value of `DDEV_PRIMARY_URL` makes "same host, other port" reachable on Knecht. **Fix.** Prefer the Knecht variable and keep the DDEV branch as fallback: ```js server: { origin: process.env.KNECHT_DEV_SERVER_URL ?? (process.env.DDEV_PRIMARY_URL ? `${process.env.DDEV_PRIMARY_URL.replace(/:\d+$/, '')}:5173` : undefined), }, ``` ## Drupal ### Dev/Dist Decision Cached Too Early **Symptom.** A preview with a dev server configured still loads `/dist/assets/...`, without `/@vite/client` or HMR. Or the tags point at `http://localhost:5173`. **Cause.** The `drupal/vite` module decides between dev server and dist build in `hook_library_info_alter`, so the decision is taken at `drush cr` and cached. With the default `useDevServer: auto`, it probes the dev server from PHP at that moment. The boot commands run `drush cr` before the dev server is up, so the probe fails and "dist" is cached. When the probe does hit, the default `devServerUrl` of `http://localhost:5173` ends up in the HTML. The module also ignores Vite's `base`, so a `base: '/dist/'` config 404s on the dev server. **Fix.** Switch explicitly in `settings.php` when Knecht hands the URL in, no probe involved: ```php if ($dev = getenv('KNECHT_DEV_SERVER_URL')) { $settings['vite'] = ['useDevServer' => TRUE, 'devServerUrl' => rtrim($dev, '/')]; } ``` In `vite.config.js`, set the base per mode: `base: command === 'serve' ? '/' : '/dist/'`. Run `drush cr` once after the change in a running session. ## Kirby ### Dev Server Origin Defaults to 0.0.0.0 **Symptom.** The preview loads, but `/@vite/client` and the app's CSS and JS are requested from `http://0.0.0.0:5173`, which the browser blocks. **Cause.** kirby-vite switches to the dev server as soon as a `.dev` marker file exists and reads the browser-facing URL from it. vite-plugin-kirby writes that file when Vite starts, with `server.origin` or, when none is set, with `://:`, which is `http://0.0.0.0:5173` for a server bound to all interfaces. **Fix.** One line in `vite.config.js`: ```js server: { origin: process.env.KNECHT_DEV_SERVER_URL, }, ``` ## Craft ### devServerPublic Is a Literal DDEV URL **Symptom.** The preview loads, but its module scripts point at `https://.ddev.site:3000/`, which the browser cannot reach. **Cause.** `config/vite.php` carries the browser-facing dev server URL as a literal, the DDEV router convention. Nothing in the Craft Vite plugin derives it from the request or from env. A second gate is `useDevServer`, usually `CRAFT_ENVIRONMENT === 'dev'`, so that variable has to be set on Knecht too. **Fix.** Read `devServerPublic` from env with a trailing slash and keep the DDEV URL as local fallback: ```php 'devServerPublic' => App::env('KNECHT_DEV_SERVER_URL') ? rtrim(App::env('KNECHT_DEV_SERVER_URL'), '/') . '/' : 'https://myproject.ddev.site:3000/', ``` `devServerInternal` stays `http://localhost:3000`, Craft probes it from PHP inside the same container. Add `CRAFT_ENVIRONMENT=dev` to the project's environment variables in Knecht. ## Custom Vite Setups ### No Dev Branch at All **Symptom.** The preview keeps loading `/dist/assets/...`, or shows a "no manifest entry" comment, whatever the dev server does. **Cause.** A hand-rolled loader reads `dist/.vite/manifest.json` and nothing else. There is no plugin that knows about a dev server, so nothing ever emits `/@vite/client`. Often the Vite config pins no port either, and a `base: '/dist/'` puts the dev URLs under `/dist/`. **Fix.** Branch in the loader: when `KNECHT_DEV_SERVER_URL` is set, emit the HMR client once plus a `` and a `