Git-flow keeps two long-lived branches, main (released) and develop (integration), and cuts three kinds of short branches off them: feature/* off develop, release/* off develop toward main, and hotfix/* off main. It trades the simplicity of trunk-based for an explicit staging branch where work accumulates before a release.

Git-flow doesn't fit one board, because a feature and a release are different lifecycles. Model it as two Shelbi Workflows: a feature workflow whose tasks branch off and merge back into develop, and a release workflow whose task PRs develop into main. Hotfixes reuse the feature-branch default with main as their base.

The board

you@hub — platform
Tasks · platform 7 total Workflow: All ▾ Workspace: All ▾ BACKLOG (2) TO DO (1) IN PROGRESS (1) REVIEW (1) ON DEVELOP (2) Draft Q3 roadmap Add SSO to admin Build billing portal Rework audit log New notifications ⎇ shelbi/build-billin…schema service ⎇ shelbi/rework-audit… Sync i18n strings Tenant-scoped API keys
h/l col j/k row open n new f filter r refresh

Every card on the feature workflow cuts its branch off develop and squash-merges back into develop. The On Develop column is "landed on the integration branch," not "released." Shipping to main is a separate release task.

The statuses

Status identity lives once in the project's status catalog, workflows/statuses.yaml. Git-flow adds one status the shipped defaults don't have: on-develop, the done-category landing for feature work that's merged into develop but not yet released. It also adds staging and released for the release workflow below. Declare them (id + name + category) in the catalog first, then reference them by id:

# workflows/statuses.yaml — git-flow statuses alongside the defaults.
statuses:
  # shipped defaults
  - { id: backlog,     name: Backlog,     category: backlog }
  - { id: todo,        name: Todo,        category: ready }
  - { id: in-progress, name: In Progress, category: active }
  - { id: review,      name: Review,      category: handoff }
  - { id: canceled,    name: Canceled,    category: archived }
  # git-flow additions
  - { id: on-develop,  name: On Develop,  category: done }
  - { id: staging,     name: Staging,     category: active }
  - { id: released,    name: Released,    category: done }

The feature workflow

# workflows/feature.yaml — reference-only; identity lives in statuses.yaml
name: feature
description: Git-flow feature — branch off develop, merge back into develop.
 
git:
  base_branch: develop                 # feature/* branches cut off develop
  merge_strategy: squash
 
statuses:
  - { id: backlog,     owner: user,  agent: orchestrator }
  - { id: todo,        owner: agent, agent: orchestrator }
  - { id: in-progress, owner: agent, agent: developer }
  - { id: review,      owner: user,  agent: orchestrator }
  - { id: on-develop,  owner: user }
  - { id: canceled,    owner: user }
 
initial_status: backlog
 
transitions:
  - from: in-progress
    to: review
    actions: [push_branch, open_pr]      # PR base is develop (the workflow base_branch)
 
  - from: review
    to: on-develop
    actions: [merge, delete_branch]      # squash into develop
 
  - from: in-progress
    to: canceled
    actions: [close_pr, delete_branch]

Setting git.base_branch: develop is the whole trick: branches are cut from develop and the open_pr action targets develop because no transition overrides it with a target:.

The release workflow

A release task PRs the accumulated develop into main. Pre-fill its branch: so the orchestrator operates on develop directly instead of cutting a new branch:

# workflows/release.yaml — reference-only; identity lives in statuses.yaml
name: release
description: Git-flow release — PR develop into main and tag.
 
git:
  base_branch: main
  merge_strategy: merge                # a true merge preserves develop's history
 
statuses:
  - { id: staging,  owner: agent, agent: developer }
  - { id: review,   owner: user,  agent: orchestrator }
  - { id: released, owner: user }
 
initial_status: staging
 
transitions:
  - from: staging
    to: review
    actions: [push_branch, open_pr]      # open PR: base main, head develop
 
  - from: review
    to: released
    actions: [merge]                     # merge develop into main; keep develop
---
id: release-2025-07
title: Release 2025.07
workflow: release
branch: develop                        # operate on develop as-is; don't cut a branch
---

Note merge_strategy: merge (not squash) and the absence of delete_branch: develop is long-lived, so the release preserves its history and never deletes it.

Orchestrator adjustments

Two routing rules keep the three lifecycles from colliding. Encode them in the orchestrator prompt (see Author a custom workflow):

## Routing rules — git-flow
 
- **Features** default to the `feature` workflow (base `develop`). Ordinary
  work goes here; do not route feature work at `main`.
- **Hotfixes** — titles prefixed `hotfix:` — use the `default` workflow
  (base `main`) so the fix branches off the released line. After a hotfix
  merges to `main`, open a follow-up task to merge `main` back into
  `develop` so the branches don't diverge.
- **Releases** are created by the user on the `release` workflow with
  `branch: develop` pre-filled. Never auto-promote a release — cutting one
  is a human decision about what's ready to ship.

The develop-back-merge after a hotfix is the one bit of bookkeeping git-flow needs that a single workflow can't express. Surface it as a follow-up task rather than trying to encode it as a transition.

See also