/process — BPMN 2.0 from Code, Anchor-Scoped, Cross-Repo-Aware

SkillDev tools

Extract a business process from registered repos via 7-axis code scan + gap-targeted interview, then emit lint-clean BPMN 2.0.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the /process — BPMN 2.0 from Code, Anchor-Scoped, Cross-Repo-Aware skill

What this skill tells your AI

The instructions your AI receives, as published by me2resh/apexyard in .claude/skills/process/SKILL.md and read by ahel’s review.

Maps a named business process from what the codebase actually does to a stakeholder-shareable BPMN 2.0 file. Discovery is anchor-scoped + reachability-bounded — the skill follows only what's connected to the operator-supplied entry point, stops at the connected-component boundary, and crosses repo boundaries only when the target is in apexyard.projects.yaml.

Pairs with /c4 (static system topology) and /extract-features (exhaustive feature inventory) as the "what we already have" tooling family. All three are read-first, ask-only-when-the-code-doesn't-say.

Runtime requirements

DependencyUsed forWithout it
bash ≥ 4The skill itselfRequired
gitAnchor resolution + workspace cloningRequired
ghCross-repo registry lookupOptional — falls back to registry-only
xmllintXML validity checkRecommended; the smoke test needs it
node + npxbpmn-auto-layout for <bpmndi> coords AND bpmnlint for the gateWithout Node: the skill emits a bare BPMN (no <bpmndi>) and warns; with --skip-lint, also bypasses the lint gate.
yqRegistry parsingFalls back to grep when absent

Same disclosure shape as docs/getting-started.md § "Optional: LSP-aware code navigation" — disclosed up front, surfaced when invoked, never silently fails.

Path resolution

Read the registry path via portfolio_registry, the per-project docs dir via portfolio_projects_dir, and the workspace dir via portfolio_workspace_dir — all from .claude/hooks/_lib-portfolio-paths.sh. Source the helper at the top of any bash block that touches those paths:

source "$(git rev-parse --show-toplevel)/.claude/hooks/_lib-read-config.sh"
source "$(git rev-parse --show-toplevel)/.claude/hooks/_lib-portfolio-paths.sh"
projects_dir=$(portfolio_projects_dir)
registry=$(portfolio_registry)
workspace_dir=$(portfolio_workspace_dir)

Defaults match the single-fork layout. Split-portfolio v2 adopters get the resolved sibling-repo paths transparently. Do not hardcode apexyard.projects.yaml or projects/ literals — the helper resolves whichever mode the adopter is in. See docs/multi-project.md.

Write targets (see me2resh/apexyard#373 + #443): paths documented as projects/<name>/X in this skill are canonical adopter-facing forms — implement them in bash as "${projects_dir}/<name>/X". Never construct from "${PWD}/projects/...", "$(git rev-parse --show-toplevel)/projects/...", or a literal ./projects/... — those break in split-portfolio v2 mode where projects_dir resolves to a sibling repo.

REQUIRED per-block preamble (see #443): Claude executes each bash block as a separate shell invocation. The projects_dir assignment from the Path resolution section above does NOT carry into later blocks. Every bash block that writes to a projects/<name>/X path MUST start with this three-line preamble so it's self-contained:

source "$(git rev-parse --show-toplevel)/.claude/hooks/_lib-read-config.sh"
source "$(git rev-parse --show-toplevel)/.claude/hooks/_lib-portfolio-paths.sh"
projects_dir=$(portfolio_projects_dir)
# ... now write to "${projects_dir}/<name>/X"

The Path resolution section's example sources the helper once for documentation purposes; it does not absolve later blocks from sourcing it themselves. Treat each bash fence as a fresh process.

Usage

# Anchored by a process slug + entry point
/process onboarding --from-endpoint POST /signup
/process checkout   --from-machine CheckoutMachine
/process invoicing  --from-job ProcessInvoiceJob
/process billing    --scope src/billing/

# Multiple anchors compose (union, then connected-component)
/process onboarding --from-endpoint POST /signup --from-machine OnboardingMachine

# Project-targeted (when not invoked from inside workspace/<name>/)
/process onboarding --project signup-svc --from-machine OnboardingMachine

# Layout opt-in (default: swimlanes within one pool)
/process onboarding --pools                 # one pool per repo, message flows between

# Re-run a previously generated process — uses anchors recorded in <slug>.process-source.md
/process onboarding                         # regenerates same scope; OFFERS (default-no) to overwrite
/process onboarding --force                 # skip the overwrite prompt

If no anchor is given AND no <slug>.process-source.md exists yet, the skill stops and asks the operator for one. The skill refuses to scan the whole repo — anchors are mandatory.

Output location

projects/<project>/processes/<slug>.bpmn               ← the BPMN 2.0 artefact (committed)
projects/<project>/processes/<slug>.process-source.md  ← discovery report + interview answers (committed)
projects/<project>/processes/README.md                 ← one-row-per-BPMN index, auto-maintained

Sibling structure to /c4's projects/<name>/architecture/ and /extract-features's projects/<name>/feature-inventory.md.

The seven discovery axes

The skill scans across seven axes — each is read-only and reachability-bounded from the anchor. Findings are printed for operator review before any file is written.

Axis 1 — Explicit workflow definitions

State machines and workflow DSLs declare the process explicitly; if one of these matches the anchor, it's the highest-signal source.

FrameworkSignature (grep fallback)
XState\.machine\.(ts|js)$; createMachine\s*\(; import .* from ['"](xstate|@xstate)['"]
Temporal@workflow.defn; @activity.defn; proxyActivities<
Cadence@workflow_method; @activity_method
AWS Step Functions\.asl\.json$; "States"\s*:; "StartAt"\s*:
Camunda / Cawemo\.bpmn$; \.cmmn$ (existing files used as starting state, not overwritten blindly)
Inngest workflowinngest\.createFunction\s*\(
Activiti / Flowable\.bpmn(20)?\.xml$

Axis 2 — Queue / job orchestration

Job chains describe deferred work — enqueue(A) → A runs → A enqueues B is a process flow.

FrameworkSignature
BullMQ flowsnew FlowProducer\s*\(; \.add\s*\(\s*\{[^}]*children:
BullMQ chainsnew Queue\s*\(; new Worker\s*\(; \.add\s*\(\s*['"][^'"]+['"]
Celery chains / chords / groupschain\s*\(; chord\s*\(; group\s*\(; \.apply_async\s*\(; @shared_task
Sidekiq workflowsclass \w+\s*\n\s*include Sidekiq::Worker; \.perform_async; sibling jobs in same module
Resque pipelinesResque\.enqueue\s*\(; class extend Resque::Plugins::*
AWS SQS handlersLambda handlers with Records[].eventSource == "aws:sqs"; SAM Events: SQS:
Inngest stepsstep\.run\s*\(; step\.sendEvent\s*\(

For each job in the connected component capture: name, trigger (queue name / event), and the downstream jobs/events it dispatches.

Axis 3 — Cron + scheduled triggers

Scheduled handlers are process entry points just like HTTP endpoints; trace what they dispatch.

SourceSignature
node-croncron\.schedule\s*\(
@nestjs/schedule@Cron\s*\(; @Interval\s*\(; @Timeout\s*\(
APSchedulerscheduler\.add_job; @scheduler\.scheduled_job
Celery beatCELERY_BEAT_SCHEDULE; beat_schedule\s*=
GitHub Actions cronon: schedule: cron: in .github/workflows/*.yml
Vercel cronvercel\.json "crons"
EventBridge / CloudWatch EventsSAM Events: Schedule:; Terraform aws_cloudwatch_event_rule with schedule_expression
Cloudflare Workers cronwrangler\.toml [triggers] crons

For each schedule capture: cron expression, handler entry point, and what the handler dispatches downstream.

Axis 4 — State-column transitions

DB columns named status, state, phase, step, stage carry process state in the schema. Grep the column's literal value set + the service-layer code that writes each transition.

SourceSignature
Prismaprisma/schema.prisma → fields named status|state|phase|step|stage
TypeORM / Sequelize@Column\s*\( on fields named status/state/...; enum types enum \w+Status
DrizzlepgEnum\s*\(; mysqlEnum\s*\(; columns named status/state/...
SQLAlchemy / Django ORMColumn\s*\(.*Enum; models.CharField.*choices=
Active Record (Rails)db/schema.rb columns named status/state; enum status:
Raw SQLCREATE TABLE columns named status/state/phase
State-writing codegrep \.status\s*=\s*['"] then trace back to the function and its callers

For each state column capture: full enum value set + the call sites that write each value. Each transition is a process edge.

Axis 5 — API choreography

Endpoint A → emit event / enqueue job → endpoint B handles. Trace across route files in the connected component.

PatternSignature
Event emissioneventBus\.emit\s*\(; EventEmitter; Outbox writes; publishEvent\s*\(
Queue dispatch from handlerroute handler body containing \.add\(.*Queue\s*\); enqueue\s*\(; .apply_async\s*\(
HTTP fan-outroute handler body containing fetch\s*\(; axios\.(get|post|...); httpx\.(get|post|...); requests\.(get|post|...); gRPC client calls
WebSocket emitio\.emit\s*\(; socket\.emit\s*\(; ws\.send\s*\(

Each event / dispatch / fan-out is a candidate edge in the process. Outbound HTTP calls + queue publishes are checked against the registry (axis 6 of the cross-repo flow below).

Axis 6 — Existing BPMN / sequence diagrams

If the project already has process diagrams, use them as starting state — never overwrite blindly. The operator may have hand-edited them.

SourceSignature
BPMN files\.bpmn(20)?(\.xml)?$ anywhere in repo
CMMN files\.cmmn(\.xml)?$
Mermaid sequence diagrams in docsdocs/processes/*.md containing ```mermaid\nsequenceDiagram
Mermaid flowcharts in docsdocs/processes/*.md containing ```mermaid\nflowchart

When an existing BPMN matches the slug or anchor, the skill loads it, parses it, and presents the existing model alongside the candidate from axes 1-5 as a diff during the candidate-review step.

Axis 7 — Documented process steps

README sections and docs/ files with headings like "Onboarding Flow" / "Order Lifecycle" or numbered-step lists are the author's own enumeration.

SourceSignature
README sections^##\s+(.*Flow|.*Process|.*Lifecycle|.*Workflow) headers + the bullet/numbered list that follows
docs/processes/**entire directory, if present
Numbered-step lists^\s*\d+\.\s+ lines within a process section

Use as confirmation / disambiguation source for step labels; don't trust as primary structure (often stale).

Process

1. Resolve the anchor + scope

Required inputs (parsed from CLI args):

  • <process-slug> — short kebab-case identifier (onboarding, checkout, invoicing). The BPMN file name, the source-of-truth name, and the registry index key.
  • At least one entry-point anchor OR an existing <slug>.process-source.md to replay from:
    • --from-endpoint METHOD /path
    • --from-machine ClassName
    • --from-job JobName
    • --scope dir/
  • Project — inferred from cwd if invoked inside workspace/<name>/; explicit via --project <name>; falls back to operator-prompt when ambiguous.

If no anchor is given AND no source-of-truth file exists, stop and ask:

Which process are we mapping?

  process slug: ____________________  (e.g. "onboarding", "checkout")
  one-line description: ____________________
  AND ONE of:
    HTTP endpoint:       e.g. POST /signup
    state machine class: e.g. OnboardingMachine
    queue job name:      e.g. ProcessSignupJob
    directory path:      e.g. src/onboarding/

2. Discover (axes 1-7, read-only)

Invoke discover.sh <project-root> --anchor=<json> --max-depth=6 (the discovery engine). It walks the seven axes, applies reachability bounding from the anchor, and outputs a structured candidate model:

{
  "anchor": { "endpoint": "POST /signup", "machine": null, "job": null, "scope": null },
  "axes_with_findings": ["1", "2", "3", "5", "7"],
  "axes_empty": ["4", "6"],
  "nodes": [
    { "id": "evt_start", "type": "event-start", "label": "Signup submitted", "source": "src/routes/signup.ts:14" },
    { "id": "task_validate", "type": "task", "label": "Validate signup payload", "source": "src/routes/signup.ts:18-32" },
    { "id": "task_create_user", "type": "task", "label": "Create user record", "source": "src/services/user.ts:45" },
    { "id": "task_send_verify", "type": "task", "label": "Send verification email", "source": "src/workers/email.ts:12 (queue: send-verify-email)" },
    { "id": "task_verify_identity", "type": "external-call", "label": "Verify identity", "source": "src/services/user.ts:60 → identity-svc#POST /verify-identity", "cross_repo": "identity-svc" },
    { "id": "gw_email_verified", "type": "gateway", "label": "Email verified?", "source": "User.emailVerifiedAt column" },
    { "id": "task_complete_profile", "type": "task", "label": "Complete profile", "source": "src/routes/profile.ts:24" },
    { "id": "evt_end", "type": "event-end", "label": "Onboarding complete", "source": "User.onboardedAt column" }
  ],
  "edges": [
    { "from": "evt_start", "to": "task_validate", "kind": "sequence" },
    { "from": "task_validate", "to": "task_create_user", "kind": "sequence" },
    { "from": "task_create_user", "to": "task_send_verify", "kind": "sequence" },
    { "from": "task_send_verify", "to": "task_verify_identity", "kind": "message", "label": "verify-identity-requested" },
    { "from": "task_verify_identity", "to": "gw_email_verified", "kind": "sequence" },
    { "from": "gw_email_verified", "to": "task_complete_profile", "kind": "sequence", "label": "yes" },
    { "from": "gw_email_verified", "to": "evt_start", "kind": "sequence", "label": "no — resend" },
    { "from": "task_complete_profile", "to": "evt_end", "kind": "sequence" }
  ],
  "external_touchpoints": [
    { "id": "ext_audit_log", "label": "Audit log service", "source": "src/services/audit.ts:8 → POST /audit-log" }
  ],
  "ambiguities": [
    { "node": "task_send_verify", "question": "user-driven, service task, or external API call?" },
    { "node": "task_verify_identity", "question": "identity-svc is registered — follow the trace into it? (default: yes)" }
  ],
  "missing_labels": [
    { "node_id": "task_step2", "current_label": "step2", "evidence": "src/routes/signup.ts:42" }
  ],
  "invisible_lanes_candidates": [
    "Admin approval — if signup matches fraud heuristics, code dispatches `ManualReviewJob` but the approver role isn't in the codebase. Confirm?"
  ]
}

3. Cross-repo trace (axes 5+ for registered repos)

When any node has cross_repo: <name> set, invoke cross-repo.sh --target=<name>:

  1. Look up the target in apexyard.projects.yaml via portfolio_registry.

  2. If the target is registered AND workspace/<name>/ exists → recurse discovery into the target with the receiving endpoint as the new anchor.

  3. If registered AND workspace/<name>/ missing → OFFER to clone (default-yes):

    Cross-repo handoff detected: signup-svc → identity-svc (POST /verify-identity)
    
    identity-svc is registered in apexyard.projects.yaml but not cloned locally.
    
    Clone now? (default: yes)
      Yes  → git clone github.com/<org>/identity-svc workspace/identity-svc (gitignored), then continue trace
      No   → render as external touchpoint with documentation
    

    Decline → render as external touchpoint with <bpmn:documentation> explaining "registered but not cloned — clone via git clone github.com/<org>/<repo> workspace/<repo> to expand on re-run".

  4. If the target is not registered → render as an external participant pool (out-of-org). Clearly marked so the trust boundary is visible.

Each repo encountered becomes a swimlane (default) or pool (when --pools set or interview-selected).

4. Present the candidate for operator review

Print the full candidate model in compact form, plus the trace report:

PROCESS: onboarding
ANCHOR:  POST /signup (signup-svc)

Discovery (5 / 7 axes had findings):
  ✓ Axis 1 — Explicit workflows: OnboardingMachine (XState) at src/onboarding/state.ts
  ✓ Axis 2 — Queues: send-verify-email (BullMQ) at src/workers/email.ts
  ✓ Axis 3 — Cron: nightly-resend-verify (node-cron) at src/workers/cleanup.ts
  ─ Axis 4 — State columns: User.status (pending|verified|complete) at prisma/schema.prisma
  ✓ Axis 5 — API choreography: POST /verify-identity → identity-svc
  ─ Axis 6 — Existing BPMN: none
  ✓ Axis 7 — Documented steps: README "Onboarding Flow" section

Scope: 12 nodes reachable from POST /signup, 3 external touchpoints not expanded
Cross-repo trail:
  signup-svc → POST /verify-identity in identity-svc → emit "identity.verified" →
  onboarding-svc subscribes → completes profile
  (3 repos in scope; identity-svc and onboarding-svc both registered, both cloned)

Candidate model:
  [Start]    "Signup submitted"
    → [Task]    "Validate signup payload"    (src/routes/signup.ts:18-32)
    → [Task]    "Create user record"         (src/services/user.ts:45)
    → [Task]    "Send verification email"    (src/workers/email.ts:12, queue: send-verify-email)
    → [Message] "verify-identity-requested" → identity-svc
    → [Task]    "Verify identity"            (identity-svc / src/routes/identity.ts:8)
    → [Gateway] "Email verified?"            (User.emailVerifiedAt)
        yes → [Task] "Complete profile"      (src/routes/profile.ts:24)
              → [End] "Onboarding complete"  (User.onboardedAt)
        no  → loop to [Start]

External touchpoints (not expanded):
  Audit log service (src/services/audit.ts:8) — expand into sub-process? [N/y]

Ambiguities to resolve (will ask in interview):
  - task_send_verify — user-driven, service task, or external API call?
  - task_step2 (src/routes/signup.ts:42) — what should this be called in the BPMN?

Invisible lanes (suggested):
  - Admin approval lane — if signup matches fraud heuristics, code dispatches
    ManualReviewJob but the approver role isn't in the codebase. Add lane? [Y/n]

Layout default: swimlanes within one pool (one lane per repo: signup-svc, identity-svc, onboarding-svc)
  Change to pools + message flows? [N/y]   ← shows trust boundaries explicitly

Accept candidate and continue to interview? (a)ccept · (e)dit · (q)uit
>

On e: enter an edit loop — operator can add/remove/relabel nodes, change edge kinds, redraw swimlane assignments, etc. On q: exit without writing any files. The discovery report is discarded (re-run regenerates). On a: continue.

5. Gap-fill interview

Iterate over ambiguities, missing_labels, and invisible_lanes_candidates from step 2. Do not ask questions whose answer is already in the discovery report.

Each interview prompt is a single question:

Q1/4: task_send_verify (src/workers/email.ts:12) — what kind of task?
  (a) service task — system-driven, no human
  (b) user task     — user has to do something
  (c) send task     — fires off a message/event
  (d) external task — handled by an external system (mark for sub-process expansion)
Choice: a

Q2/4: task_step2 (src/routes/signup.ts:42) — what should this step be called in the BPMN?
Label: Persist signup metadata

Q3/4: Add admin-approval lane? (fraud-heuristic dispatch goes to ManualReviewJob; no in-code approver role)
[Y/n]: y
  Lane name (default: "Fraud review"): Compliance review

Q4/4: External touchpoint "Audit log service" — sub-process or black-box?
  (a) sub-process — expand inline
  (b) black-box   — single shape, no expansion
Choice: b

Answers are appended to the source-of-truth markdown for replay.

6. Generate BPMN 2.0 XML

Invoke generate-bpmn.sh <project-root> <source-of-truth.md> -o <output.bpmn>:

  1. Build the XML tree from the final node + edge model:
    • <bpmn:definitions> root with targetNamespace="http://apexyard/process/<slug>"
    • One <bpmn:collaboration> containing per-repo pools (when --pools) or one pool with per-repo <bpmn:lane>s (default).
    • One <bpmn:process> per pool (default: one process).
    • <bpmn:startEvent> / <bpmn:endEvent> for events; <bpmn:task> / <bpmn:serviceTask> / <bpmn:userTask> / <bpmn:sendTask> for tasks; <bpmn:exclusiveGateway> / <bpmn:parallelGateway> for gateways.
    • <bpmn:sequenceFlow> for sequence edges; <bpmn:messageFlow> for cross-pool / message edges (only valid inside <bpmn:collaboration>, not inside <bpmn:process>).
    • Every element carries a <bpmn:documentation> child citing source evidence (src/onboarding/state.ts:42-58, cron config, operator input).
  2. Pipe the bare XML through npx -y bpmn-auto-layout to populate <bpmndi:BPMNDiagram> coords. If Node is unavailable, emit without the <bpmndi> block and warn — Camunda Modeler shows "blank" but the file is still semantically valid; operators with Node can re-run later.

7. Lint and the auto-fix / re-interview / accept loop

Invoke lint.sh <output.bpmn>:

  1. Run npx -y bpmnlint <output.bpmn> with the ruleset:

    • bpmnlint/recommended base
    • label-required (every flow node has a label)
    • no-disconnected (every node is on a path from a start event to an end event)
    • no-implicit-split (multiple outgoing sequence flows need an explicit gateway)
    • Overridable via .bpmnlintrc in the project root.
  2. If violations exist, surface each with file-line context, then offer:

    bpmnlint reported 2 violations:
    
    [ERROR] label-required — task_step2 has no label
      element: bpmn:Task id="task_step2" (line 47)
    
    [WARN]  no-implicit-split — task_create_user has 2 outgoing flows without a gateway
      element: bpmn:Task id="task_create_user" (line 52)
    
    How to proceed?
      (a) auto-fix where possible — adds placeholder labels, inserts exclusiveGateway for splits
      (r) re-interview affected nodes
      (s) accept with documented exception — adds <bpmn:documentation> stating the override
      (q) quit, leave .bpmn unmodified
    Choice:
    
  3. On a: apply auto-fixes (labels from node.id slugified; gateways inserted with <bpmn:documentation> "auto-inserted by /process to satisfy no-implicit-split"); re-run lint.

  4. On r: jump back to step 5 for only the affected nodes.

  5. On s: write the exception to .bpmnlintrc (per-rule, per-element-id), document the exception in the BPMN's <bpmn:documentation>, re-run lint.

  6. On q: stop. Source-of-truth markdown is still written so progress isn't lost.

The skill exits successfully only when bpmnlint --max-warnings 0 returns 0. Use --skip-lint to bypass (the operator owns the consequences).

8. Write outputs

Three files:

  1. projects/<project>/processes/<slug>.bpmn — the artefact
  2. projects/<project>/processes/<slug>.process-source.md — the discovery report + interview answers + final candidate model + lint disposition
  3. projects/<project>/processes/README.md — append/update the index row

If <slug>.bpmn already exists:

projects/<project>/processes/<slug>.bpmn already exists (last written 2026-04-12).
Overwrite? [N/y]

Default N. On N, write to <slug>-<YYYY-MM-DD>.bpmn and tell the operator where the new file landed. The original is preserved. With --force, skip the prompt.

9. Report

✓ /process onboarding complete

  Artefact:  projects/signup-svc/processes/onboarding.bpmn
  Source:    projects/signup-svc/processes/onboarding.process-source.md
  Index:     projects/signup-svc/processes/README.md

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
498
Forks
271
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
process-me2resh
Source
github.com/me2resh/apexyard