Customizing the Workflow
/.trellis/workflow.md is the single source of truth for how Trellis runs development. Phase definitions, skill routing, per-turn reminders, and the task.py command catalog all live in this one file. Forking the workflow means editing one markdown file — no Python, no hook code, no re-releasing Trellis.
Before 0.5, workflow behavior was scattered across three places: hook Python scripts, configurator TypeScript, and command markdown. Keeping them in sync while forking was painful. 0.5 collapses the three into workflow.md.
What workflow.md controls
Section in workflow.md | Who reads it | Effect |
|---|
## Phase Index + ## Phase 1/2/3 | AI at session start (via SessionStart hook) | Defines the three-phase flow and the step-by-step how-to inside each phase |
### Skill Routing | AI at session start | Maps user intent → which skill to load (e.g. “wants new feature” → trellis-brainstorm) |
### DO NOT skip skills | AI at session start | Inlines the common excuses for skipping skills and the reasons why they’re wrong |
## Workflow State Breadcrumbs | inject-workflow-state.py on every UserPromptSubmit | Per-turn reminder shown as <workflow-state>…</workflow-state> based on task status |
### Task System (task.py command list) | AI at session start | Reference for the 16 task.py subcommands grouped by purpose |
All injection paths read workflow.md at runtime — you don’t need to rebuild anything after editing.
Changing the per-turn breadcrumbs
The per-turn <workflow-state> block is what nudges the AI at each message based on the current task’s status field. You can rewrite any of them without touching the hook script.
## Workflow State Breadcrumbs
[workflow-state:no_task]
No active task. If the user describes multi-step work, load trellis-brainstorm skill…
[/workflow-state:no_task]
[workflow-state:planning]
Complete prd.md via trellis-brainstorm skill; then run task.py start.
[/workflow-state:planning]
[workflow-state:in_progress]
Flow: implement → check → update-spec → finish
Check conversation history + git status to determine current step; do NOT skip check.
[/workflow-state:in_progress]
[workflow-state:completed]
User commits changes; then run task.py archive.
[/workflow-state:completed]
Rules:
- Tag
STATUS matches task.json.status. Defaults: planning / in_progress / completed, plus no_task when no task is active.
- Hyphens and underscores are both allowed in the tag (
blocked / in-review / needs_qa etc.).
- If a status has no matching tag block, the hook falls back to a built-in default — safe to delete blocks you don’t need.
- Keep each block short (~200 bytes). This is injected every turn; bloat means the AI pays attention cost on every message.
Adding a custom workflow state
Want a blocked state that nudges the AI to escalate instead of guessing?
[workflow-state:blocked]
Task is blocked waiting on external input. Don't start new implementation —
surface the blocker to the user or spawn `trellis-research` if more investigation helps.
[/workflow-state:blocked]
Then set the status on the task by editing task.json directly:
# Switch to a custom status
jq '.status = "blocked"' "$TASK_DIR/task.json" > "$TASK_DIR/task.json.tmp" && mv "$TASK_DIR/task.json.tmp" "$TASK_DIR/task.json"
From the next message onward, the AI sees the blocked breadcrumb injected per turn. Switch it back to in_progress the same way to resume the normal flow.
Built-in task.py subcommands only transition between the default statuses (start → in_progress, archive → completed). Custom statuses are plain strings in task.json.status — the breadcrumb system doesn’t require pre-registering them, and task.py list --status <name> can filter by any string.
Changing the skill routing table
The table under ### Skill Routing is what the AI consults when deciding whether to load an auto-trigger skill.
| User intent | Skill |
|----------------------------------------------|----------------------|
| Wants a new feature / requirement unclear | trellis-brainstorm |
| About to write code / start implementing | trellis-before-dev |
| Finished writing / want to verify | trellis-check |
| Stuck / fixed same bug several times | trellis-break-loop |
| Spec needs update | trellis-update-spec |
Add a row for a custom skill you shipped (see Chapter 12: Custom Skills):
| User wants a test plan before writing code | trellis-test-plan |
No code change needed — next session, the AI reads the updated table and routes correctly.
Adding or reshaping a Phase
The Phase sections in workflow.md are plain markdown. You can:
- Add a Phase 4: Review — define steps (4.1, 4.2, …) and their how-to text. Reference it from breadcrumbs (
[workflow-state:in_review]).
- Split Plan into two branches (A/B) — replace step numbering with 1A.1 / 1B.1 naming; the AI follows what’s in the text.
- Shorten Finish — delete steps you don’t care about (e.g. drop 3.2 debug retrospective).
Keep the Phase Index in sync with the detailed Phase sections — the index is inlined at SessionStart so the AI sees both consistently.
get_context.py --mode phase --step X.Y parses ## Phase X headings + #### X.Y step headings to extract the step body. If you rename or reshape sections, make sure the step anchors still parse (heading depth is significant).
What NOT to edit
A handful of conventions are consumed by scripts and shouldn’t drift:
| Don’t change | Why |
|---|
The tag format [workflow-state:STATUS]…[/workflow-state:STATUS] | inject-workflow-state.py parses this literally |
Heading depth for Phase / step (## Phase X + #### X.Y) | get_context.py --mode phase --step X.Y depends on it |
task.py subcommand names in the task lifecycle list | They must match the actual CLI; renaming in docs doesn’t rename the script |
Everything else — phrasing, ordering, adding sections, rewriting how-to text — is fair game.
When your change takes effect
| Change | When AI sees it |
|---|
| Breadcrumb text | Next user message (re-read on every UserPromptSubmit) |
| Phase / step body | Next session (re-read on SessionStart) |
| Skill routing table | Next session |
Forks don’t need to republish anything. Commit your edited workflow.md to the repo and your team picks up the change on the next session start.