Read and write Shortcut.com stories, epics, and workflows via the REST API v3.
Token is stored at ~/.openclaw/secrets/shortcut (mode 600, readable only by your user).
SHORTCUT_API_TOKEN=$(cat ~/.openclaw/secrets/shortcut 2>/dev/null)
BASE="https://api.app.shortcut.com/api/v3"
If empty, ask the user for their API token, then save it:
mkdir -p ~/.openclaw/secrets
echo -n "<token>" > ~/.openclaw/secrets/shortcut && chmod 600 ~/.openclaw/secrets/shortcut
Generate a token at app.shortcut.com → Settings → API Tokens. Shortcut tokens have full member-level access — no scope restriction is available. Rotate or delete the token at any time from the same settings page.
> If you prefer not to persist the token on disk, skip saving and export it for the session only:
> export SHORTCUT_API_TOKEN="
Always use jq -n --arg / --argjson to build request bodies. Never interpolate user-supplied values directly into shell strings — this prevents shell injection from values containing quotes, backticks, or $().
# ✅ Safe — jq handles all escaping
DATA=$(jq -n --arg name "$TITLE" --arg desc "$DESCRIPTION" \
'{name: $name, description: $desc}')
curl -s -X POST -H "Shortcut-Token: $SHORTCUT_API_TOKEN" \
-H "Content-Type: application/json" -d "$DATA" "$BASE/stories"
# ❌ Unsafe — never do this
curl ... -d "{\"name\": \"$TITLE\"}"
curl -s -H "Shortcut-Token: $SHORTCUT_API_TOKEN" "$BASE/stories/<id>" | \
jq '{id, name, story_type, description, workflow_state_id, estimate, epic_id, labels: [.labels[].name]}'
Strip the sc- prefix from IDs (use the number only).
curl -s -H "Shortcut-Token: $SHORTCUT_API_TOKEN" \
"$BASE/search/stories?$(jq -rn --arg q "$QUERY" 'query=\($q|@uri)&page_size=10')" | \
jq '.data[] | {id, name, story_type, estimate}'
ME=$(curl -s -H "Shortcut-Token: $SHORTCUT_API_TOKEN" "$BASE/member" | jq -r '.id')
curl -s -H "Shortcut-Token: $SHORTCUT_API_TOKEN" \
"$BASE/search/stories?owner_id=${ME}&page_size=25" | \
jq '.data[] | {id, name, story_type, workflow_state_id}'
curl -s -H "Shortcut-Token: $SHORTCUT_API_TOKEN" "$BASE/workflows" | \
jq '.[] | {workflow: .name, states: [.states[] | {id, name, type}]}'
curl -s -H "Shortcut-Token: $SHORTCUT_API_TOKEN" "$BASE/epics" | \
jq '.[] | {id, name, state, total_stories: .stats.num_stories_total}'
curl -s -H "Shortcut-Token: $SHORTCUT_API_TOKEN" "$BASE/epics/<epic_id>/stories" | \
jq '.[] | {id, name, story_type, workflow_state_id, estimate}'
curl -s -H "Shortcut-Token: $SHORTCUT_API_TOKEN" "$BASE/groups" | \
jq '[.[] | {id, name, mention_name}]'
All write operations build JSON with jq -n --arg to safely handle user-supplied strings.
DATA=$(jq -n \
--arg name "$STORY_TITLE" \
--arg description "$STORY_DESCRIPTION" \
--arg story_type "$STORY_TYPE" \
--argjson estimate "$ESTIMATE" \
--argjson workflow_state_id "$STATE_ID" \
--arg group_id "$GROUP_ID" \
--argjson epic_id "$EPIC_ID" \
'{name: $name, description: $description, story_type: $story_type,
estimate: $estimate, workflow_state_id: $workflow_state_id,
group_id: $group_id, epic_id: $epic_id}')
curl -s -X POST \
-H "Shortcut-Token: $SHORTCUT_API_TOKEN" \
-H "Content-Type: application/json" \
-d "$DATA" "$BASE/stories" | jq '{id, name, app_url}'
Story types: feature, bug, chore
To add labels, extend the jq expression:
DATA=$(jq -n --arg name "$TITLE" --arg label "mobile" \
'{name: $name, labels: [{name: $label}]}')
DATA=$(jq -n \
--arg name "$EPIC_TITLE" \
--arg description "$EPIC_DESCRIPTION" \
--arg group_id "$GROUP_ID" \
'{name: $name, description: $description, group_id: $group_id}')
curl -s -X POST \
-H "Shortcut-Token: $SHORTCUT_API_TOKEN" \
-H "Content-Type: application/json" \
-d "$DATA" "$BASE/epics" | jq '{id, name, app_url}'
DATA=$(jq -n --argjson state "$NEW_STATE_ID" '{workflow_state_id: $state}')
curl -s -X PUT \
-H "Shortcut-Token: $SHORTCUT_API_TOKEN" \
-H "Content-Type: application/json" \
-d "$DATA" "$BASE/stories/$STORY_ID" | jq '{id, name, workflow_state_id}'
DATA=$(jq -n --arg text "$COMMENT_TEXT" '{text: $text}')
curl -s -X POST \
-H "Shortcut-Token: $SHORTCUT_API_TOKEN" \
-H "Content-Type: application/json" \
-d "$DATA" "$BASE/stories/$STORY_ID/comments" | jq '{id, text}'
DATA=$(jq -n \
--argjson object_id "$BLOCKED_ID" \
--argjson subject_id "$BLOCKER_ID" \
'{object_id: $object_id, subject_id: $subject_id, verb: "blocks"}')
curl -s -X POST \
-H "Shortcut-Token: $SHORTCUT_API_TOKEN" \
-H "Content-Type: application/json" \
-d "$DATA" "$BASE/story-links" | jq '{id, verb}'
Common default state IDs (verify with the workflows endpoint for your workspace):
| State | Typical ID |
|---|---|
| ------- | ----------- |
| Backlog | 500000008 |
| Ready for Development | 500000007 |
| In Development | 500000006 |
| Ready for Review | 500000010 |
| Completed | 500000011 |
Always confirm state IDs by calling /workflows — they vary per account.
For creating full epics with multiple dependent stories, see references/create-stories.md.
Stories:
🎯 sc-1234 — User Authentication Flow [feature, 5pts]
Status: In Development | Epic: Auth & Onboarding
Labels: backend, mobile
> Users should be able to log in with email/password...
Backlog list (table format):
| ID | Story | Type | Pts | State |
|---------|--------------------------|---------|-----|---------|
| sc-1234 | User Auth Flow | feature | 5 | In Dev |
| sc-1235 | Fix password reset email | bug | 2 | Backlog |
sc- prefix from story IDs for API calls/stories/ directly, not search共 1 个版本