Files
PaperClipAI/docs/pipelines-tutorial.md
T
2026-06-09 23:28:48 +00:00

12 KiB

Pipelines Tutorial: Release to Content

This walkthrough uses only the Paperclip CLI to run the release-to-content casework flow:

  • one release case fans out into 10 content suggestion cases
  • a review inbox approves 7, rejects 2 with reasons, and edits then approves 1
  • approved cases fire a drafting routine on stage entry
  • a human accepts an agent transition suggestion
  • a blocker prevents premature publishing until the upstream case is done
  • the release rollup reports a complete done/cancelled split
  • event history shows provenance for the case

Prerequisites

Run this against a dev Paperclip instance with a board token or an agent token that can manage pipelines and routines.

export PAPERCLIP_API_URL=http://localhost:3100
export PAPERCLIP_COMPANY_ID=<company-id>
export PAPERCLIP_API_KEY=<token>

# Optional: assign the drafting routine to an existing agent.
export DRAFTING_AGENT_ID=<agent-id>

Use a unique suffix so the tutorial can be rerun without pipeline key collisions:

export RUN_KEY="$(date +%Y%m%d%H%M%S)"
export RELEASES_PIPELINE="releases-$RUN_KEY"
export CONTENT_PIPELINE="content-$RUN_KEY"

Create The Drafting Routine

cat > /tmp/drafting-routine.json <<JSON
{
  "title": "Draft approved content",
  "description": "Use the embedded Pipeline Case Context to draft the approved content case, then transition the case when the draft is ready.",
  "priority": "medium",
  "status": "active",
  "concurrencyPolicy": "always_enqueue",
  "catchUpPolicy": "skip_missed"
  ${DRAFTING_AGENT_ID:+, "assigneeAgentId": "$DRAFTING_AGENT_ID"}
}
JSON

export DRAFTING_ROUTINE_PAYLOAD="$(jq -c . /tmp/drafting-routine.json)"
export DRAFTING_ROUTINE_ID="$(
  paperclipai routine create \
    -C "$PAPERCLIP_COMPANY_ID" \
    --payload-json "$DRAFTING_ROUTINE_PAYLOAD" \
    --json | jq -r '.id'
)"

Create Pipelines

cat > /tmp/releases-stages.json <<'JSON'
[
  { "key": "released", "name": "Released", "kind": "open", "position": 100 },
  { "key": "complete", "name": "Complete", "kind": "done", "position": 900 },
  { "key": "cancelled", "name": "Cancelled", "kind": "cancelled", "position": 1000 }
]
JSON

paperclipai pipelines create \
  -C "$PAPERCLIP_COMPANY_ID" \
  --key "$RELEASES_PIPELINE" \
  --name "Releases $RUN_KEY" \
  --stages-file /tmp/releases-stages.json
cat > /tmp/content-stages.json <<'JSON'
[
  {
    "key": "review",
    "name": "Human review",
    "kind": "review",
    "position": 100,
    "config": {
      "approveToStageKey": "drafting",
      "rejectToStageKey": "cancelled",
      "requireRejectReason": true,
      "reviewerKind": "human"
    }
  },
  { "key": "drafting", "name": "Drafting", "kind": "working", "position": 200 },
  { "key": "ready", "name": "Ready to publish", "kind": "working", "position": 300 },
  { "key": "published", "name": "Published", "kind": "done", "position": 900 },
  { "key": "cancelled", "name": "Cancelled", "kind": "cancelled", "position": 1000 }
]
JSON

paperclipai pipelines create \
  -C "$PAPERCLIP_COMPANY_ID" \
  --key "$CONTENT_PIPELINE" \
  --name "Content $RUN_KEY" \
  --stages-file /tmp/content-stages.json

Configure transitions, guidance, and the drafting automation:

cat > /tmp/content-transitions.json <<'JSON'
{
  "transitions": [
    { "fromStageKey": "review", "toStageKey": "drafting", "label": "approve" },
    { "fromStageKey": "review", "toStageKey": "cancelled", "label": "reject" },
    { "fromStageKey": "drafting", "toStageKey": "ready", "label": "draft ready" },
    { "fromStageKey": "drafting", "toStageKey": "published", "label": "publish directly" },
    { "fromStageKey": "drafting", "toStageKey": "cancelled", "label": "cancel" },
    { "fromStageKey": "ready", "toStageKey": "published", "label": "publish" }
  ]
}
JSON

paperclipai pipelines set-transitions \
  -C "$PAPERCLIP_COMPANY_ID" \
  "$CONTENT_PIPELINE" \
  --file /tmp/content-transitions.json

cat > /tmp/content-guidance.md <<'MD'
# Content pipeline guidance

Approve content suggestions that can teach users what changed and reject suggestions
that are too narrow, duplicative, or disconnected from the release.
MD

paperclipai pipelines guidance put \
  -C "$PAPERCLIP_COMPANY_ID" \
  "$CONTENT_PIPELINE" \
  --file /tmp/content-guidance.md

paperclipai pipelines set-automation \
  -C "$PAPERCLIP_COMPANY_ID" \
  "$CONTENT_PIPELINE" \
  --stage drafting \
  --routine "$DRAFTING_ROUTINE_ID"

Ingest The Release And Suggestions

export RELEASE_CASE_ID="$(
  paperclipai pipelines ingest \
    -C "$PAPERCLIP_COMPANY_ID" \
    "$RELEASES_PIPELINE" \
    --case-key "release-$RUN_KEY" \
    --title "Release $RUN_KEY: Pipeline primitive" \
    --summary "A release case that fans out into content suggestions." \
    --fields-json '{"release":"v0.pipeline-tutorial"}' \
    --json | jq -r '.case.id'
)"

Create the 10 parented suggestion cases. This batch file is included verbatim; only $RELEASE_CASE_ID is substituted by the shell.

jq -n --arg parent "$RELEASE_CASE_ID" '{
  items: [
    { caseKey: "suggestion-01", title: "Launch blog post", summary: "Explain the release end to end.", fields: { channel: "blog" }, parentCaseId: $parent, stageKey: "review" },
    { caseKey: "suggestion-02", title: "Product changelog", summary: "Concise changelog entry.", fields: { channel: "docs" }, parentCaseId: $parent, stageKey: "review" },
    { caseKey: "suggestion-03", title: "Launch tweet", summary: "Short tweet after the blog is ready.", fields: { channel: "x" }, parentCaseId: $parent, stageKey: "review" },
    { caseKey: "suggestion-04", title: "Customer email", summary: "Email announcement.", fields: { channel: "email" }, parentCaseId: $parent, stageKey: "review" },
    { caseKey: "suggestion-05", title: "LinkedIn post", summary: "Professional network announcement.", fields: { channel: "linkedin" }, parentCaseId: $parent, stageKey: "review" },
    { caseKey: "suggestion-06", title: "Discord update", summary: "Community update.", fields: { channel: "discord" }, parentCaseId: $parent, stageKey: "review" },
    { caseKey: "suggestion-07", title: "Release notes", summary: "Detailed release notes.", fields: { channel: "release_notes" }, parentCaseId: $parent, stageKey: "review" },
    { caseKey: "suggestion-08", title: "Overly niche post", summary: "Reject: too niche.", fields: { channel: "blog" }, parentCaseId: $parent, stageKey: "review" },
    { caseKey: "suggestion-09", title: "Duplicate tweet", summary: "Reject: duplicate.", fields: { channel: "x" }, parentCaseId: $parent, stageKey: "review" },
    { caseKey: "suggestion-10", title: "Short-form video", summary: "Needs edit before approval.", fields: { channel: "video" }, parentCaseId: $parent, stageKey: "review" }
  ]
}' > /tmp/content-suggestions.json

paperclipai pipelines ingest-batch \
  -C "$PAPERCLIP_COMPANY_ID" \
  "$CONTENT_PIPELINE" \
  --file /tmp/content-suggestions.json \
  --json | tee /tmp/content-suggestions-result.json

Review The Inbox

paperclipai pipelines review-inbox \
  -C "$PAPERCLIP_COMPANY_ID" \
  --pipeline "$CONTENT_PIPELINE"

Approve seven and reject two in bulk:

case_id() {
  jq -r --arg key "$1" '.[] | select(.case.caseKey == $key) | .case.id' /tmp/content-suggestions-result.json
}

export CASE_01="$(case_id suggestion-01)"
export CASE_02="$(case_id suggestion-02)"
export CASE_03="$(case_id suggestion-03)"
export CASE_04="$(case_id suggestion-04)"
export CASE_05="$(case_id suggestion-05)"
export CASE_06="$(case_id suggestion-06)"
export CASE_07="$(case_id suggestion-07)"
export CASE_08="$(case_id suggestion-08)"
export CASE_09="$(case_id suggestion-09)"
export CASE_10="$(case_id suggestion-10)"

jq -n \
  --arg c1 "$CASE_01" --arg c2 "$CASE_02" --arg c3 "$CASE_03" --arg c4 "$CASE_04" --arg c5 "$CASE_05" \
  --arg c6 "$CASE_06" --arg c7 "$CASE_07" --arg c8 "$CASE_08" --arg c9 "$CASE_09" '{
  items: [
    { caseId: $c1, decision: "approve", expectedVersion: 1 },
    { caseId: $c2, decision: "approve", expectedVersion: 1 },
    { caseId: $c3, decision: "approve", expectedVersion: 1 },
    { caseId: $c4, decision: "approve", expectedVersion: 1 },
    { caseId: $c5, decision: "approve", expectedVersion: 1 },
    { caseId: $c6, decision: "approve", expectedVersion: 1 },
    { caseId: $c7, decision: "approve", expectedVersion: 1 },
    { caseId: $c8, decision: "reject", reason: "Too niche for this launch.", expectedVersion: 1 },
    { caseId: $c9, decision: "reject", reason: "Duplicates the launch tweet.", expectedVersion: 1 }
  ]
}' > /tmp/review-decisions.json

paperclipai pipelines review-bulk \
  -C "$PAPERCLIP_COMPANY_ID" \
  --file /tmp/review-decisions.json

Edit and approve the tenth item:

paperclipai pipelines case review \
  -C "$PAPERCLIP_COMPANY_ID" \
  "$CASE_10" \
  --approve \
  --expected-version 1 \
  --title "Edited short-form video" \
  --fields-json '{"channel":"video","edited":true}'

Approved cases enter drafting, which fires the configured routine and links automation issues. Inspect one:

paperclipai pipelines case get \
  -C "$PAPERCLIP_COMPANY_ID" \
  "$CASE_01"

Accept An Agent Suggestion

export SUGGESTION_ID="$(
  paperclipai pipelines case suggest \
    -C "$PAPERCLIP_COMPANY_ID" \
    "$CASE_01" \
    --to ready \
    --rationale "Draft is complete enough to publish." \
    --confidence 0.9 \
    --json | jq -r '.suggestion.id'
)"

paperclipai pipelines case resolve-suggestion \
  -C "$PAPERCLIP_COMPANY_ID" \
  "$CASE_01" \
  --suggestion "$SUGGESTION_ID" \
  --accept \
  --expected-version 2

paperclipai pipelines case transition \
  -C "$PAPERCLIP_COMPANY_ID" \
  "$CASE_01" \
  --to published \
  --expected-version 3 \
  --reason "Suggestion accepted and content published."

Demonstrate Blockers And The 409 Guard

The tweet waits for the blog post:

paperclipai pipelines case block \
  -C "$PAPERCLIP_COMPANY_ID" \
  "$CASE_03" \
  --by "$CASE_02"

# This fails with code=blocked and prints a recovery hint.
paperclipai pipelines case transition \
  -C "$PAPERCLIP_COMPANY_ID" \
  "$CASE_03" \
  --to published \
  --expected-version 2 \
  --reason "Try before blog is published."

Resolve the blocker and publish the tweet:

paperclipai pipelines case transition \
  -C "$PAPERCLIP_COMPANY_ID" \
  "$CASE_02" \
  --to published \
  --expected-version 2 \
  --reason "Blog published; unblocks tweet."

paperclipai pipelines case transition \
  -C "$PAPERCLIP_COMPANY_ID" \
  "$CASE_03" \
  --to published \
  --expected-version 2 \
  --reason "Blocker resolved."

Complete The Rollup

Cancel one approved case, publish the rest, then inspect the release rollup:

paperclipai pipelines case transition -C "$PAPERCLIP_COMPANY_ID" "$CASE_04" --to cancelled --expected-version 2 --reason "Cancelled after review."
paperclipai pipelines case transition -C "$PAPERCLIP_COMPANY_ID" "$CASE_05" --to published --expected-version 2 --reason "Published."
paperclipai pipelines case transition -C "$PAPERCLIP_COMPANY_ID" "$CASE_06" --to published --expected-version 2 --reason "Published."
paperclipai pipelines case transition -C "$PAPERCLIP_COMPANY_ID" "$CASE_07" --to published --expected-version 2 --reason "Published."
paperclipai pipelines case transition -C "$PAPERCLIP_COMPANY_ID" "$CASE_10" --to published --expected-version 3 --reason "Edited and published."

paperclipai pipelines case rollup \
  -C "$PAPERCLIP_COMPANY_ID" \
  "$RELEASE_CASE_ID" \
  --json

Expected rollup:

{
  "total": 10,
  "done": 7,
  "cancelled": 3,
  "open": 0,
  "complete": true
}

Dump Provenance

paperclipai pipelines case events \
  -C "$PAPERCLIP_COMPANY_ID" \
  "$CASE_01" \
  --json

Every event includes an actor field. Human actions record a user actor, agent actions require a run id, and system actions record system. For this case, look for ingested, review_decided, transition_suggested, suggestion_resolved, and transitioned.

Scripted Smoke

The same flow is available as a smoke script:

PAPERCLIP_API_URL=http://localhost:3100 \
PAPERCLIP_COMPANY_ID=<company-id> \
PAPERCLIP_API_KEY=<token> \
pnpm smoke:pipelines-tutorial

The smoke assigns the drafting routine to DRAFTING_AGENT_ID when set. If it is not set, it uses PAPERCLIP_AGENT_ID when present, otherwise the first non-terminated company agent returned by the CLI, so the routine automation can create linked drafting issues.