Skip to content
Back to skills

Github Project Board

ASecurity

Create and standardize a GitHub Projects v2 board to the toolkit's canonical Kanban config (Status/Priority/Size/Start/Target fields; Table/Kanban/Roadmap views). Provisions a board when none exists and reconciles missing fields/options on an existing one — always asking before it creates or mutates. Used to bootstrap the board the issue, delegate, and github-project-management skills then consume.

  • 2 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 19, 2026
ai-agentsgobashnodegitapisecurity

Works with

  • cli
  • api

Security analysis

A100/100

Scanned September 19, 2026

npx -y skills add dustinkeeton/wafflestack --skill github-project-board --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Github Project Board?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Github Project Board
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/dustinkeeton-github-project-board-wafflestack/badge)](https://www.skillsdirectory.com/skills/dustinkeeton-github-project-board-wafflestack)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: github-project-board
description: Create and standardize a GitHub Projects v2 board to the toolkit's canonical Kanban config (Status/Priority/Size/Start/Target fields; Table/Kanban/Roadmap views). Provisions a board when none exists and reconciles missing fields/options on an existing one — always asking before it creates or mutates. Used to bootstrap the board the issue, delegate, and github-project-management skills then consume.
user-invocable: true
argument-hint: "(omit to target the board matching the project name) | <project number or title to standardize>"
---

# GitHub Project Board — provision & standardize

The other board-touching skills only **consume** an existing Projects v2 board — `delegate`
syncs Kanban status, `issue` files new items to "Backlog", and `github-project-management`
holds the read/update GraphQL catalog. None of them **create** or **standardize** a board.
This skill fills that gap: it provisions a board to the toolkit's **standard spec** when none
exists, and reconciles a partial board up to it — **never silently**. It always asks before it
creates a board or mutates an existing one.

Use it once, at setup, to bootstrap the board the consumer skills then rely on. For the full
read/update-item query catalog (statuses, dates, milestones, sprint planning) see the
`github-project-management` skill — this skill covers only the board *structure*.

## Prerequisites

- `gh` CLI authenticated **with the `project` scope** — board creation and field mutations
  need it beyond the usual `repo` scope. If a call fails with a message about the `project`
  scope, grant it and retry:
  ```bash
  gh auth refresh -s project
  ```
- Repo owner / name / node ID:
  ```bash
  OWNER=$(gh repo view --json owner -q .owner.login)
  REPO=$(gh repo view --json name -q .name)
  REPO_ID=$(gh repo view --json id -q .id)   # GraphQL node ID (R_…), used to link the board
  ```
- Owner node ID (works for a user- or org-owned repo):
  ```bash
  OWNER_ID=$(gh api graphql -f query='
    query($owner: String!) { repositoryOwner(login: $owner) { id } }
  ' -f owner="$OWNER" --jq '.data.repositoryOwner.id')
  ```

## The standard board spec

Encoded here as the canonical target (modeled on the "Obsidian Synapse" reference board that
`github-project-management`'s date heuristics already assume). "Standardizing" a board means
reconciling it toward this:

| Field | Type | Options / notes |
|-------|------|-----------------|
| **Status** | single-select (Kanban columns) | Backlog · Todo · In Progress · In Review · Done |
| **Priority** | single-select | Critical · High · Medium · Low |
| **Size** | single-select | S · M · L |
| **Start** | date | Roadmap bar start |
| **Target** | date | Roadmap bar end |

**Views:** Table (all items) · Kanban (board layout grouped by Status) · Roadmap (Start→Target).
**Milestones:** used when scoped, from the repo's existing milestone catalog (see
`github-project-management`). **Swimlanes:** none by default (the reference board uses none) —
an optional later refinement.

> **New boards start with only a Status field** (GitHub seeds it with Todo / In Progress /
> Done). Everything else — Backlog & In Review options, Priority, Size, Start, Target — is
> added by this skill.

## Decision flow — ask first, always

1. **Discover** the board matching the project name (below).
2. **A board already exists** → do **not** silently mutate it. Report what it has vs. the
   standard spec (missing fields, missing Status options, missing views) and **ask** the user
   which to add: e.g. "add the standard config as an additional view/board, and/or add the
   missing Priority & Size fields and Backlog/In Review Status options?" Apply only what they
   approve.
3. **No board exists** → **ask** before creating one. On approval: create it, link it to the
   repo, then provision the fields/options and set up the views.

Never create or mutate without an explicit go-ahead.

## Discover the board (case-insensitive, normalized)

An account may own several projects, and the title may differ from `project.name` only by case
(`WaffleStack` vs `wafflestack`). Match on a **normalized, exact** title — downcase both sides —
rather than the substring regex the consumer skills use, so a longer project name can't
false-match:

```bash
PROJECT_ID=$(gh api graphql -f query='
  query($owner: String!) {
    user(login: $owner) {
      projectsV2(first: 50) { nodes { id number title } }
    }
  }
' -f owner="$OWNER" \
  --jq 'first(.data.user.projectsV2.nodes[]
          | select((.title | ascii_downcase) == ("{{project.name}}" | ascii_downcase))
          | .id) // empty')
```

For an organization-owned repo, replace `user(login: $owner)` with `organization(login: $owner)`.
`PROJECT_ID` empty → no matching board (the "no board" path). Non-empty → the "existing board"
path; introspect its fields/options with the **Get project fields** query in
`github-project-management` before deciding what is missing.

## Create a board (no-board path, after approval)

```bash
PROJECT_ID=$(gh api graphql -f query='
  mutation($ownerId: ID!, $title: String!) {
    createProjectV2(input: {ownerId: $ownerId, title: $title}) {
      projectV2 { id number url }
    }
  }
' -f ownerId="$OWNER_ID" -f title="{{project.name}}" \
  --jq '.data.createProjectV2.projectV2.id')
```

Link it to the repo so it appears under the repo's Projects tab and the consumer skills find it:

```bash
gh api graphql -f query='
  mutation($projectId: ID!, $repositoryId: ID!) {
    linkProjectV2ToRepository(input: {projectId: $projectId, repositoryId: $repositoryId}) {
      repository { id }
    }
  }
' -f projectId="$PROJECT_ID" -f repositoryId="$REPO_ID"
```

## Provision fields

`createProjectV2Field` creates a **new** field. Single-select options are inlined in the
mutation body (not passed as variables — `gh api -f` only carries scalar variables); each option
needs **`name`, `color`, `description`** (all required) and `color` is an unquoted enum from
`{GRAY, BLUE, GREEN, YELLOW, ORANGE, RED, PINK, PURPLE}`.

**Priority** (single-select):

```bash
gh api graphql -f query='
  mutation($projectId: ID!) {
    createProjectV2Field(input: {
      projectId: $projectId
      dataType: SINGLE_SELECT
      name: "Priority"
      singleSelectOptions: [
        {name: "Critical", color: RED,    description: "Crash, data loss, security, blocks all users"}
        {name: "High",     color: ORANGE, description: "Broken workflow, regression, significant UX issue"}
        {name: "Medium",   color: YELLOW, description: "New feature, improvement, moderate bug"}
        {name: "Low",      color: BLUE,   description: "Cosmetic, nice-to-have, minor, tech debt"}
      ]
    }) {
      projectV2Field { ... on ProjectV2SingleSelectField { id name options { id name } } }
    }
  }
' -f projectId="$PROJECT_ID"
```

**Size** (single-select) — same shape, `name: "Size"` and options
`{name: "S", color: GREEN, …} {name: "M", color: YELLOW, …} {name: "L", color: ORANGE, …}`.

**Start** and **Target** (date fields — one call each):

```bash
gh api graphql -f query='
  mutation($projectId: ID!) {
    createProjectV2Field(input: {projectId: $projectId, dataType: DATE, name: "Start"}) {
      projectV2Field { ... on ProjectV2Field { id name } }
    }
  }
' -f projectId="$PROJECT_ID"
# repeat with name: "Target"
```

Valid `dataType` values: `TEXT`, `SINGLE_SELECT`, `NUMBER`, `DATE`, `ITERATION`.

## Reconcile the Status field (the tricky one)

The auto-created Status field usually has **Todo / In Progress / Done**; the standard spec adds
**Backlog** and **In Review**. Options are edited with `updateProjectV2Field`, which is a
**full replace** — you must resend **every existing option plus the additions** in one array, or
the options you omit are deleted. First read the current options (the **Get project fields** query
in `github-project-management`, capturing the `Status` field's `id` and each option's `name`),
then:

```bash
gh api graphql -f query='
  mutation($fieldId: ID!) {
    updateProjectV2Field(input: {
      fieldId: $fieldId
      singleSelectOptions: [
        {name: "Backlog",     color: GRAY,   description: "Not yet scheduled"}
        {name: "Todo",        color: BLUE,   description: "Scheduled, not started"}
        {name: "In Progress", color: YELLOW, description: "Actively being worked"}
        {name: "In Review",   color: ORANGE, description: "In a PR / awaiting review"}
        {name: "Done",        color: GREEN,  description: "Merged / closed"}
      ]
    }) {
      projectV2Field { ... on ProjectV2SingleSelectField { id options { id name } } }
    }
  }
' -f fieldId="$STATUS_FIELD_ID"
```

**Honest caveats — verify before you run this on a live board:**

- **Full replace regenerates option IDs.** Items already assigned to an option can be
  **orphaned** (their Status clears) when the option set is rewritten. This is harmless on a
  **fresh** board (no items yet) — the common case for this skill — but on a **populated** board
  it can wipe assignments. There, prefer editing the Status options **in the GitHub UI**, or
  proceed only with the user's explicit understanding of the risk.
- **The built-in Status field is the most restricted** — some accounts still reject
  add/delete/rename of its options via the API. If the mutation errors, fall back to the manual
  UI steps below rather than fighting it.
- **Same read-first, full-array pattern** applies to adding a missing option to *any* existing
  single-select field (e.g. a Priority someone hand-created without "Critical").

## Views — not creatable via the public API

Projects v2 **views cannot be created via the public GraphQL API** (no `createProjectV2View`
mutation exists). Two ways to get the Table / Kanban / Roadmap views:

**A. Copy a template board (recommended when you keep one).** `copyProjectV2` clones a source
project's **fields, options, *and* views** in one call — so if you maintain a canonical template
board, copy it instead of building field-by-field, then link the copy to the repo:

```bash
gh api graphql -f query='
  mutation($ownerId: ID!, $projectId: ID!, $title: String!) {
    copyProjectV2(input: {
      ownerId: $ownerId, projectId: $projectId, title: $title, includeDraftIssues: false
    }) {
      projectV2 { id number url }
    }
  }
' -f ownerId="$OWNER_ID" -f projectId="$TEMPLATE_PROJECT_ID" -f title="{{project.name}}"
```

**B. Guided manual steps (no template).** Print these for the user to click through on the
board's web UI (`…/projects/<number>`):

1. **Table** — the default view; rename it "Table" and show all items.
2. **Kanban** — New view → **Board** layout → group by **Status**. Columns follow the Status
   options (Backlog → Todo → In Progress → In Review → Done).
3. **Roadmap** — New view → **Roadmap** layout → set the date fields to **Start** and **Target**.

## Verify

Re-run the **Get project fields** query (`github-project-management`) and confirm Status,
Priority, Size, Start, and Target are present with the expected options; open the board and
confirm the three views. Report field/option/view coverage vs. the standard spec, and note
anything left to the manual UI (views without a template, or Status options the API refused).

## Error handling

- **Missing `project` scope** → `gh auth refresh -s project`, then retry (never widen scopes
  without the user's go-ahead).
- **GraphQL errors** — check the response for `.errors` (see the `github-project-management`
  error-handling pattern) and surface the message; don't assume success from a `0` exit.
- **Ambiguous discovery** (several projects match after normalization) → list them (number +
  title) and ask which to standardize rather than guessing.
- Board provisioning is a deliberate, user-approved action — unlike the consumer skills'
  best-effort board sync, do **not** silently skip on failure; report what succeeded and what
  still needs a manual step.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…