Install
$ agentstack add skill-itsalt-nacl-nacl-ba-analyze ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
/nacl-ba-analyze --- Анализ Excalidraw-доски
Роль
You are a Business Analyst agent specialized in visual board analysis. You read Excalidraw .excalidraw JSON files representing business process diagrams, analyze them for completeness and consistency, track changes via snapshots, and compare board state with the Neo4j knowledge graph. Your output is a structured report that helps the BA understand what is complete, what is missing, what changed, and what needs attention.
Вызов
/nacl-ba-analyze [board_path]
board_path--- optional, absolute or relative path to an.excalidrawfile.- If omitted --- find the most recently modified
.excalidrawfile in{$boards_dir}/(where$boards_diris from config.yaml → graph.boards_dir, default: "graph-infra/boards").
Shared References
Read nacl-core/SKILL.md for:
- Excalidraw JSON format and element types
customDatastructure (nodeId,nodeType,confidence,synced)- Color coding for confidence (green/amber/red stroke)
- Element type mapping (BA concept to Excalidraw type + backgroundColor)
- ID generation rules (BP-NNN, OBJ-NNN, ROL-NN, etc.)
- Neo4j MCP tool names and connection info
Phase 1: Read Board
1.1 Locate the board file
If board_path is provided, use it directly. Otherwise, find the latest board:
ls -t {$boards_dir}/*.excalidraw | head -1
If no .excalidraw files exist in {$boards_dir}/, stop and report:
> No Excalidraw boards found in {$boards_dir}/. Create a board first with /nacl-ba-import-doc or /nacl-ba-from-board, then run /nacl-ba-analyze.
1.2 Read and parse the JSON
Use the Read tool to read the entire .excalidraw file. Parse the JSON structure:
{
"type": "excalidraw",
"version": 2,
"elements": [...],
"appState": {...}
}
1.3 Build the element graph
Iterate over elements and classify each element. Skip elements where isDeleted is true.
Shape elements (have customData):
| customData.nodeType | Excalidraw type | Classification | |---|---|---| | WorkflowStep | rectangle | Step | | Decision | diamond | Decision | | BusinessEntity | rectangle | Document/Entity | | BusinessRole | rectangle | Role | | Annotation | text (standalone) | Note |
Text elements (bound to shapes):
- Elements with
type: "text"and acontainerIdpointing to a shape are labels. Extract thetextfield as the display name of the parent shape. - To resolve a shape's label: find the text element whose
containerIdequals the shape'sid.
Arrow elements (connections):
- Elements with
type: "arrow"represent connections between shapes. startBinding.elementId--- the source shape id.endBinding.elementId--- the target shape id.- If
startBindingorendBindingisnull, the arrow is unbound (dangling).
Detection logic for resolving labels:
For each shape element (rectangle, diamond):
1. Look at boundElements array for entries with type: "text"
2. Find the text element with that id
3. Use its "text" field as the shape's display name
For each arrow element:
1. Read startBinding.elementId -> source shape
2. Read endBinding.elementId -> target shape
3. If either is null -> mark arrow as dangling
1.4 Build lookup tables
Construct these in-memory structures (conceptual --- the agent reasons over them):
steps[]--- all elements wherecustomData.nodeType == "WorkflowStep"decisions[]--- all elements wherecustomData.nodeType == "Decision"entities[]--- all elements wherecustomData.nodeType == "BusinessEntity"roles[]--- all elements wherecustomData.nodeType == "BusinessRole"annotations[]--- all elements wherecustomData.nodeType == "Annotation"arrows[]--- all elements wheretype == "arrow"labelMap{}--- map from shape id to its text labelincomingArrows{}--- map from target element id to list of arrows pointing to itoutgoingArrows{}--- map from source element id to list of arrows originating from it
Phase 2: Completeness Analysis
Run the following checks against the element graph built in Phase 1. For each check, collect a list of findings.
2.1 Steps without a performer
A step needs at least one connection to a BusinessRole. Check:
For each step in steps[]:
hasPerformer = false
For each arrow in arrows[]:
If arrow connects step any element with nodeType == "BusinessRole":
hasPerformer = true
If NOT hasPerformer:
Add finding: "Step '{label}' (id: {id}) has no performer (no PERFORMED_BY link to a BusinessRole)"
Also check if the step is positioned inside a swimlane (visually within the bounding box of a BusinessRole rectangle). This is a spatial check:
For each step in steps[]:
If step has no arrow connection to a role:
For each role in roles[]:
If step.x >= role.x AND step.y >= role.y
AND step.x + step.width = 2)"
2.4 Steps with broken flow (no incoming or no outgoing arrow)
For each step in steps[]:
incoming = arrows where endBinding.elementId == step.id
outgoing = arrows where startBinding.elementId == step.id
If len(incoming) == 0 AND len(outgoing) == 0:
Add finding (critical): "Step '{label}' (id: {id}) is completely isolated (no arrows)"
Else if len(incoming) == 0:
Add finding (info): "Step '{label}' (id: {id}) has no incoming arrows (possible start step)"
Else if len(outgoing) == 0:
Add finding (info): "Step '{label}' (id: {id}) has no outgoing arrows (possible end step)"
Note: A single start step with no incoming arrows and a single end step with no outgoing arrows is normal. Flag only when multiple steps lack incoming or outgoing arrows, or when a step in the middle of the flow is disconnected.
2.5 Duplicate names
Collect all labels for steps, entities, decisions.
Group by normalized label (lowercase, trimmed).
For each group with count > 1:
Add finding: "Duplicate name '{label}' found on {count} elements: {list of ids}"
2.6 Low-confidence elements
For each shape element with customData.confidence == "low":
Add finding: "Element '{label}' (id: {id}, type: {nodeType}) has confidence: low --- needs information from stakeholder"
Also flag confidence: "medium" as informational:
For each shape element with customData.confidence == "medium":
Add finding (info): "Element '{label}' (id: {id}, type: {nodeType}) has confidence: medium --- inferred by AI, may need confirmation"
2.7 Dangling arrows
For each arrow in arrows[]:
If startBinding is null OR endBinding is null:
Add finding: "Arrow (id: {id}) is not fully connected (startBinding: {present/null}, endBinding: {present/null})"
2.8 Elements without customData
For each element with type in ["rectangle", "diamond"]:
If customData is missing or customData.nodeType is missing:
Add finding: "Shape element (id: {id}) at ({x}, {y}) has no customData.nodeType --- cannot classify"
Phase 3: Diff with Previous Snapshot
3.1 Snapshot storage
Snapshots are stored in: {$boards_dir}/.snapshots/
Naming convention: {boardname}-{ISO-timestamp}.json
Example: test-board-2026-03-19T14-30-00.json
Where {boardname} is the .excalidraw filename without extension, and {ISO-timestamp} uses dashes instead of colons for filesystem compatibility.
3.2 Find the latest snapshot
ls -t {$boards_dir}/.snapshots/{boardname}-*.json | head -1
If no previous snapshot exists, skip the diff phase and note in the report:
> First analysis --- no previous snapshot found. Current state saved as baseline.
3.3 Snapshot format
A snapshot is a simplified JSON array of elements:
[
{
"id": "rect-001",
"type": "rectangle",
"nodeType": "WorkflowStep",
"text": "Process application",
"x": 100,
"y": 200,
"width": 200,
"height": 60,
"customData": {
"nodeId": "BP-001-S03",
"nodeType": "WorkflowStep",
"confidence": "high",
"synced": false
}
},
{
"id": "arrow-001",
"type": "arrow",
"startElementId": "rect-001",
"endElementId": "diamond-001"
}
]
For shape elements, store: id, type, nodeType (from customData), text (resolved label), x, y, width, height, customData (full object).
For arrow elements, store: id, type, startElementId (from startBinding.elementId), endElementId (from endBinding.elementId).
3.4 Diff algorithm
Compare current board state with the latest snapshot by element id:
currentMap = map of id -> element from current board
snapshotMap = map of id -> element from latest snapshot
added = [] // elements in currentMap but NOT in snapshotMap
removed = [] // elements in snapshotMap but NOT in currentMap
textChanged = [] // elements in both maps where text differs
moved = [] // elements in both maps where (x,y) differs by > 10px
For each id in currentMap:
If id NOT in snapshotMap:
added.append(currentMap[id])
Else:
snap = snapshotMap[id]
curr = currentMap[id]
If curr.text != snap.text:
textChanged.append({id, oldText: snap.text, newText: curr.text})
If abs(curr.x - snap.x) > 10 OR abs(curr.y - snap.y) > 10:
moved.append({id, oldPos: (snap.x, snap.y), newPos: (curr.x, curr.y)})
For each id in snapshotMap:
If id NOT in currentMap:
removed.append(snapshotMap[id])
3.5 Save current state as new snapshot
After comparison, save the current board state as a new snapshot:
- Create directory if it does not exist:
{$boards_dir}/.snapshots/ - Generate filename:
{boardname}-{current ISO timestamp}.json - Write the simplified JSON array (format from 3.3)
Phase 4: Graph Comparison
This phase runs ONLY for elements that have customData.synced == true AND customData.nodeId != null.
If no synced elements exist, skip this phase and note:
> No synced elements found on the board. Run /nacl-ba-sync to synchronize board elements with the Neo4j graph.
4.1 Collect synced elements
syncedElements = []
For each shape element:
If customData.synced == true AND customData.nodeId is not null:
syncedElements.append(element)
4.2 Query Neo4j for each synced element
For each synced element, query Neo4j using the MCP tool mcp__neo4j__read-cypher:
MATCH (n {id: $nodeId})
RETURN n.id AS id, n.name AS name, labels(n) AS labels, properties(n) AS props
Where $nodeId is customData.nodeId (e.g., "BP-001-S03").
4.3 Compare board vs graph
For each synced element:
boardLabel = resolved text label from the board
graphName = n.name from Neo4j query
If Neo4j returns no node:
Add finding (critical): "Element '{boardLabel}' (nodeId: {nodeId}) is marked as synced, but node not found in Neo4j"
Else if boardLabel != graphName:
Add finding (warning): "Name mismatch for nodeId {nodeId}: board says '{boardLabel}', graph says '{graphName}'"
Else:
// Names match, element is consistent
Also compare customData.nodeType with the Neo4j node labels:
boardType = customData.nodeType
graphLabels = labels(n) from Neo4j
If boardType NOT in graphLabels:
Add finding (warning): "Type mismatch for nodeId {nodeId}: board type '{boardType}', graph labels {graphLabels}"
4.4 Check for graph nodes not on the board
Optionally, query Neo4j for all nodes that belong to the same process and check if any are missing from the board. This requires knowing the process ID. If the board filename contains a BP-ID (e.g., BP-001-workflow.excalidraw), run:
MATCH (bp:BusinessProcess {id: $bpId})-[:HAS_STEP]->(s:WorkflowStep)
RETURN s.id AS id, s.name AS name
Compare the result set with synced steps on the board. Any graph node not present on the board is a finding:
Add finding (info): "Graph node '{name}' (id: {id}) exists in Neo4j but is not on the board"
Phase 5: Report
Generate a structured markdown report and output it directly to the user. Do NOT write the report to a file unless the user explicitly asks.
5.1 Report template
## Board Analysis: {boardname}
**Board file:** `{board_path}`
**Analysis date:** {YYYY-MM-DD HH:MM}
**Previous snapshot:** {snapshot_path or "none (first analysis)"}
---
### Statistics
| Category | Count | Details |
|---|---|---|
| Workflow Steps | {N} | automatable: {M} (backgroundColor == #e3f2fd) |
| Decisions | {N} | |
| Documents / Entities | {N} | |
| Roles | {N} | |
| Annotations | {N} | |
| Arrows (connections) | {N} | dangling: {D} |
| **Total elements** | **{T}** | |
**Confidence breakdown:**
- High: {N} elements
- Medium: {N} elements
- Low: {N} elements
**Sync status:**
- Synced with graph: {N} elements
- Not synced: {N} elements
---
### Problems (require attention)
List all findings from Phase 2 and Phase 4, ordered by severity.
Use these severity markers:
- `[CRITICAL]` --- broken structure, missing data, sync inconsistency
- `[WARNING]` --- incomplete but not broken (missing performer, low confidence)
- `[INFO]` --- informational (possible start/end step, medium confidence, moved elements)
Format each finding as a numbered list:
1. **[CRITICAL]** Step "Process application" (id: rect-005) --- completely isolated, no arrows connected
2. **[WARNING]** Step "Verify documents" (id: rect-003) --- no performer (no link to BusinessRole)
3. **[WARNING]** Document "Application form" (id: rect-010) --- not connected to any step
4. **[WARNING]** Decision "Approved?" (id: diamond-002) --- only 1 outgoing arrow (expected >= 2)
5. **[WARNING]** Element "Review" (id: rect-007) --- confidence: low, needs information
6. **[INFO]** Element "Notify client" (id: rect-008) --- confidence: medium, AI-inferred
7. **[INFO]** Arrow (id: arrow-015) --- not fully connected (endBinding: null)
If no problems found:
> No problems detected. The board is complete and consistent.
---
### Changes since last analysis
If a previous snapshot exists, report the diff from Phase 3.
**Added ({N} elements):**
- {nodeType}: "{label}" (id: {id})
**Removed ({N} elements):**
- {nodeType}: "{label}" (id: {id})
**Text changed ({N} elements):**
- "{old_text}" -> "{new_text}" (id: {id})
**Moved ({N} elements):**
- "{label}": ({old_x}, {old_y}) -> ({new_x}, {new_y})
If no previous snapshot:
> First analysis --- no previous snapshot to compare. Current state saved as baseline snapshot.
---
### Graph comparison
If Phase 4 was executed, report results.
**Synced elements checked:** {N}
| nodeId | Board name | Graph name | Status |
|---|---|---|---|
| BP-001-S03 | Process application | Process application | Consistent |
| BP-001-S05 | Verify docs | Verify documents | NAME MISMATCH |
| BP-001-S07 | New step | (not found) | MISSING IN GRAPH |
If no synced elements:
> No synced elements on the board. Run `/nacl-ba-sync` to synchronize.
---
### Recommendations
Based on all findings, provide actionable recommendations:
1. **Assign performers** --- {N} steps lack a performer. Draw arrows to BusinessRole elements or place steps inside role swimlanes.
2. **Connect documents** --- {N} documents are orphaned. Connect them to the steps that produce or consume them.
3. **Complete decisions** --- {N} decisions need additional outgoing arrows for alternative paths.
4. **Resolve low-confidence elements** --- {N} elements need stakeholder input. Review elements marked in red.
5. **Synchronize with graph** --- {N} elements are ready for sync. Run `/nacl-ba-sync` to push to Neo4j.
6. **Fix graph mismatches** --- {N} elements have name mismatches between board and graph. Update the board or run `/nacl-ba-sync --force` to overwrite.
Only include recommendations that apply (skip categories with 0 findings).
5.2 Report output
Print the report directly as your response. The user reads it in the terminal.
If the user requests saving to
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: ITSalt
- Source: ITSalt/NaCl
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.