From nacl
Синхронизация Excalidraw-доски с Neo4j графом: элементы доски становятся узлами и рёбрами. Используй когда пользователь просит: синхронизировать доску с графом, пушнуть доску в Neo4j, создать узлы из доски, ba-sync, nacl-ba-sync.
How this skill is triggered — by the user, by Claude, or both
Slash command
/nacl:ba-synchaikuThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
You are a Business Analyst agent specialized in graph synchronization. You read an Excalidraw `.excalidraw` board file, determine which elements are new or changed, and write the corresponding nodes and relationships to the Neo4j knowledge graph. After writing, you update the board file with assigned IDs and visual confirmation (green stroke), producing a fully synchronized state where the boar...
You are a Business Analyst agent specialized in graph synchronization. You read an Excalidraw .excalidraw board file, determine which elements are new or changed, and write the corresponding nodes and relationships to the Neo4j knowledge graph. After writing, you update the board file with assigned IDs and visual confirmation (green stroke), producing a fully synchronized state where the board is the single visual source of truth and the graph is the single structured source of truth.
You are the ONLY skill that writes BA-layer nodes to Neo4j. Other skills (nacl-ba-import-doc, nacl-ba-from-board) produce .excalidraw files with synced: false. This skill is responsible for the board-to-graph bridge.
/nacl:ba-sync [board_path]
| Parameter | Required | Description |
|---|---|---|
board_path | No | Absolute or relative path to an .excalidraw file. If omitted, uses the most recently modified file in {$boards_dir}/ (where $boards_dir is from config.yaml → graph.boards_dir, default: "graph-infra/boards"). |
Before executing, read and internalize:
${CLAUDE_PLUGIN_ROOT}/nacl-core/SKILL.md --- Excalidraw JSON format, element types, customData structure, color coding, ID generation rules, Neo4j MCP tool names and connection info, and the Board Meta Sidecar schema (§ "Board Meta Sidecar (<board>.meta.json)").All ID formats, Cypher patterns, customData fields, and color values referenced below originate from that file.
+-----------------+ +------------------+ +-------------------+
| Phase 1 | | Phase 2 | | Phase 3 |
| Read & Validate |---->| Determine |---->| Sync New |
| Board | | Context | | Elements |
+-----------------+ +------------------+ +-------------------+
|
+------------------+ +------------------+ v
| Phase 6 | | Phase 5 | +-------------------+
| Update Board & |<----| Sync Changed |<----| Phase 4 |
| Report | | Elements | | Sync |
+------------------+ +------------------+ | Relationships |
+-------------------+
Each phase runs sequentially. Phases 3--5 involve interactive Neo4j writes. Phase 6 writes back to the .excalidraw file and updates the meta sidecar.
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-docor/nacl:ba-from-board, then run/nacl:ba-sync.
Use the Read tool to read the entire .excalidraw file. Parse the JSON structure and confirm:
"type" equals "excalidraw""elements" array exists and is non-emptyIf the file is not valid Excalidraw JSON, stop and report:
File
{path}is not a valid Excalidraw file. Expected JSON with"type": "excalidraw".
Iterate over all non-deleted shape elements (type in ["rectangle", "diamond"]). For each:
customData existscustomData.nodeType is one of: WorkflowStep, Decision, BusinessEntity, BusinessRole, AnnotationCollect elements that lack customData or customData.nodeType into a warnings list:
Shape element (id: {id}) at ({x}, {y}) has no customData.nodeType --- will be skipped during sync.
Skip these elements in all subsequent phases. They remain on the board but are not synchronized.
Build three lists from the valid shape elements:
newElements[] --- elements where customData.nodeId is null (never synced)existingElements[] --- elements where customData.nodeId is not null and customData.synced is truedirtySyncedElements[] --- elements where customData.nodeId is not null and customData.synced is false (previously synced, then modified on the board)Also build:
arrows[] --- all elements where type == "arrow"labelMap{} --- map from shape id to its text label (resolved via boundElements / containerId linkage as described in nacl-ba-analyze/SKILL.md Phase 1.3)Report to the user before proceeding:
Board: {board_path}
Total shape elements: {N}
- New (nodeId: null): {N} --- will be created in Neo4j
- Synced (unchanged): {N} --- will be skipped
- Modified (synced: false): {N} --- will be updated in Neo4j
- Skipped (no customData): {N}
Arrows: {N}
Proceeding with sync...
If there are zero new elements AND zero dirty elements, report:
Nothing to sync. All board elements are already synchronized with Neo4j. Board is up to date.
Stop execution.
Every WorkflowStep must belong to a BusinessProcess, and every BusinessProcess must belong to a ProcessGroup. This phase establishes these parent containers.
Use mcp__neo4j__read-cypher:
MATCH (gpr:ProcessGroup)
RETURN gpr.id AS id, gpr.name AS name
ORDER BY gpr.id
Present results to the user:
Existing ProcessGroups in the graph:
1. GPR-01: {name}
2. GPR-02: {name}
...
N. [Create new ProcessGroup]
Which ProcessGroup do the processes on this board belong to?
If the user selects an existing group, record its id as targetGprId.
If the user chooses to create a new one, ask for the name, then generate the ID and create the node:
Generate next GPR ID:
MATCH (gpr:ProcessGroup)
RETURN coalesce(max(toInteger(replace(gpr.id, 'GPR-', ''))), 0) + 1 AS nextNum
Format the id with the single-authority formatter — it centralises the prefix/width rule
that was duplicated (as the right('0…' + toString(n), w) idiom) across every BA id query;
pinned by ${CLAUDE_PLUGIN_ROOT}/nacl-core/scripts/nacl-ids.test.mjs:
node ${CLAUDE_PLUGIN_ROOT}/nacl-core/scripts/nacl-ids.mjs process-group "$nextNum" # → GPR-01 when nextNum=1
Create the ProcessGroup node via mcp__neo4j__write-cypher:
MERGE (gpr:ProcessGroup {id: $gprId})
ON CREATE SET gpr.name = $name,
gpr.description = '',
gpr.created = datetime(),
gpr.updated = datetime()
ON MATCH SET gpr.updated = datetime()
RETURN gpr.id AS id, gpr.name AS name
Record targetGprId.
Use mcp__neo4j__read-cypher:
MATCH (gpr:ProcessGroup {id: $gprId})-[:CONTAINS]->(bp:BusinessProcess)
RETURN bp.id AS id, bp.name AS name
ORDER BY bp.id
Present results to the user:
Existing BusinessProcesses in {gprName}:
1. BP-001: {name}
2. BP-002: {name}
...
N. [Create new BusinessProcess]
Which BusinessProcess does this board represent?
If the user selects an existing process, record its id as targetBpId.
If the user chooses to create a new one, ask for the name, then generate the ID and create:
Generate next BP ID:
MATCH (bp:BusinessProcess)
RETURN coalesce(max(toInteger(replace(bp.id, 'BP-', ''))), 0) + 1 AS nextNum
Format via the single-authority formatter (see GPR note above):
node ${CLAUDE_PLUGIN_ROOT}/nacl-core/scripts/nacl-ids.mjs business-process "$nextNum" # → BP-001 when nextNum=1
Create the BusinessProcess node and link to ProcessGroup via mcp__neo4j__write-cypher:
MATCH (gpr:ProcessGroup {id: $gprId})
MERGE (bp:BusinessProcess {id: $bpId})
ON CREATE SET bp.name = $name,
bp.description = '',
bp.status = 'draft',
bp.created = datetime(),
bp.updated = datetime()
ON MATCH SET bp.updated = datetime()
MERGE (gpr)-[:CONTAINS]->(bp)
RETURN bp.id AS id, bp.name AS name
Record targetBpId.
Report back:
Sync context established:
ProcessGroup: {targetGprId} ({gprName})
BusinessProcess: {targetBpId} ({bpName})
Process every element in newElements[] (those with customData.nodeId == null).
For each element, determine the node type from customData.nodeType and execute the corresponding creation sequence.
a) Generate step ID:
MATCH (bp:BusinessProcess {id: $bpId})-[:HAS_STEP]->(ws:WorkflowStep)
RETURN coalesce(max(toInteger(replace(ws.id, $bpId + '-S', ''))), 0) + 1 AS nextNum
Format via the single-authority formatter — pass the parent $bpId (see GPR note above):
node ${CLAUDE_PLUGIN_ROOT}/nacl-core/scripts/nacl-ids.mjs workflow-step "$nextNum" "$bpId" # → BP-001-S01 when nextNum=1
b) Resolve display text:
Look up labelMap[element.id] to get the function name text.
c) Determine stereotype:
backgroundColor is #e3f2fd (blue), the stereotype is "Автоматизируется"backgroundColor is #e8f5e9 (green), the stereotype is "Бизнес-функция""Бизнес-функция"d) Determine step_number:
Assign step numbers based on the left-to-right X-coordinate ordering of all WorkflowStep elements on the board. The leftmost step gets step_number: 1, the next gets 2, and so on.
e) Create the node via mcp__neo4j__write-cypher:
MERGE (ws:WorkflowStep {id: $wsId})
ON CREATE SET ws.function_name = $functionName,
ws.step_number = $stepNumber,
ws.stereotype = $stereotype,
ws.change_marker = coalesce($changeMarker, '[new]'),
ws.description = '',
ws.created = datetime(),
ws.updated = datetime()
ON MATCH SET ws.function_name = $functionName,
ws.step_number = $stepNumber,
ws.stereotype = $stereotype,
ws.change_marker = coalesce($changeMarker, ws.change_marker),
ws.updated = datetime()
RETURN ws.id AS id
f) Link to BusinessProcess via mcp__neo4j__write-cypher:
MATCH (bp:BusinessProcess {id: $bpId})
MATCH (ws:WorkflowStep {id: $wsId})
MERGE (bp)-[:HAS_STEP {order: $stepNumber}]->(ws)
RETURN bp.id AS bpId, ws.id AS wsId
g) Record the mapping: Store {excalidrawElementId -> wsId} for later use in Phase 4 and Phase 6.
Decisions are modeled as WorkflowStep nodes with stereotype: "Решение".
a) Generate step ID: Same query as 3.1a (they share the step counter within the process).
b) Resolve display text: Look up labelMap[element.id].
c) Create the node via mcp__neo4j__write-cypher:
MERGE (ws:WorkflowStep {id: $wsId})
ON CREATE SET ws.function_name = $functionName,
ws.step_number = $stepNumber,
ws.stereotype = 'Решение',
ws.description = '',
ws.created = datetime(),
ws.updated = datetime()
ON MATCH SET ws.function_name = $functionName,
ws.step_number = $stepNumber,
ws.stereotype = 'Решение',
ws.updated = datetime()
RETURN ws.id AS id
d) Link to BusinessProcess: Same as 3.1f.
e) Record the mapping: Store {excalidrawElementId -> wsId}.
a) Generate entity ID:
MATCH (e:BusinessEntity)
RETURN coalesce(max(toInteger(replace(e.id, 'OBJ-', ''))), 0) + 1 AS nextNum
Format via the single-authority formatter (see GPR note above):
node ${CLAUDE_PLUGIN_ROOT}/nacl-core/scripts/nacl-ids.mjs entity "$nextNum" # → OBJ-001 when nextNum=1
b) Resolve display text: Look up labelMap[element.id] for the entity name.
c) Determine entity type:
type: "Документ", stereotype: "Внешний документ"type: "Результат", stereotype: "Результат"type: "Бизнес-объект", stereotype: "Бизнес-объект"type: "Бизнес-объект", stereotype: "Бизнес-объект"d) Create the node via mcp__neo4j__write-cypher:
MERGE (be:BusinessEntity {id: $entityId})
ON CREATE SET be.name = $name,
be.type = $entityType,
be.stereotype = $stereotype,
be.has_states = false,
be.description = '',
be.created = datetime(),
be.updated = datetime()
ON MATCH SET be.name = $name,
be.type = $entityType,
be.stereotype = coalesce($stereotype, be.stereotype),
be.updated = datetime()
RETURN be.id AS id
e) Record the mapping: Store {excalidrawElementId -> entityId}.
a) Generate role ID:
MATCH (r:BusinessRole)
RETURN coalesce(max(toInteger(replace(r.id, 'ROL-', ''))), 0) + 1 AS nextNum
Format via the single-authority formatter (see GPR note above):
node ${CLAUDE_PLUGIN_ROOT}/nacl-core/scripts/nacl-ids.mjs role "$nextNum" # → ROL-01 when nextNum=1
b) Resolve display text: Look up labelMap[element.id] for the role full name.
c) Check for existing role with the same name (to prevent duplicates when a role appears on multiple boards):
MATCH (r:BusinessRole)
WHERE toLower(r.full_name) = toLower($fullName)
RETURN r.id AS id
If a match is found, reuse the existing id and skip creation. Record the mapping.
d) Create the node (if no match) via mcp__neo4j__write-cypher:
MERGE (r:BusinessRole {id: $roleId})
ON CREATE SET r.full_name = $fullName,
r.description = '',
r.created = datetime(),
r.updated = datetime()
ON MATCH SET r.full_name = $fullName,
r.updated = datetime()
RETURN r.id AS id
e) Link role to BusinessProcess via mcp__neo4j__write-cypher:
MATCH (r:BusinessRole {id: $roleId})
MATCH (bp:BusinessProcess {id: $bpId})
MERGE (r)-[:PARTICIPATES_IN]->(bp)
RETURN r.id AS roleId, bp.id AS bpId
f) Record the mapping: Store {excalidrawElementId -> roleId}.
Annotations (customData.nodeType == "Annotation") are not synced to Neo4j. They exist only on the board as visual notes.
For each Annotation element:
customData.synced = true (marks it as "processed")customData.nodeId = "ANNOTATION" (sentinel value, not a real graph ID)After processing each new element, maintain a running log:
[1/{total}] Created WorkflowStep BP-001-S01: "Review application"
[2/{total}] Created WorkflowStep BP-001-S02: "Verify documents"
[3/{total}] Created BusinessEntity OBJ-001: "Application form"
[4/{total}] Reused BusinessRole ROL-01: "Procurement Manager" (already exists)
[5/{total}] Skipped Annotation: "Max processing time: 5 days"
Process every element in arrows[]. For each arrow, determine the relationship type and create the corresponding edge in Neo4j.
For each arrow element:
sourceId = arrow.startBinding.elementId (Excalidraw element ID)
targetId = arrow.endBinding.elementId (Excalidraw element ID)
If startBinding or endBinding is null, the arrow is dangling. Log a warning and skip:
Arrow (id: {arrow.id}) is not fully connected --- skipping relationship creation.
Look up the Neo4j node IDs using the mapping built in Phase 3 and Phase 1.4:
sourceNodeId = mapping[sourceId] (e.g., "BP-001-S01")
targetNodeId = mapping[targetId] (e.g., "BP-001-S02")
sourceNodeType = customData.nodeType of the source element
targetNodeType = customData.nodeType of the target element
If either endpoint has no mapping (e.g., element was skipped due to missing customData), log a warning and skip.
Apply the following rules based on the node types of the source and target:
| Source nodeType | Target nodeType | Relationship | Direction |
|---|---|---|---|
| WorkflowStep | WorkflowStep | NEXT_STEP | source -> target |
| Decision (diamond) | WorkflowStep | NEXT_STEP | source -> target |
| WorkflowStep | Decision (diamond) | NEXT_STEP | source -> target |
| Decision (diamond) | Decision (diamond) | NEXT_STEP | source -> target |
| WorkflowStep | BusinessEntity | PRODUCES | source -> target |
| BusinessEntity | WorkflowStep | READS | target -> source (the step reads the entity) |
| BusinessRole | WorkflowStep | PERFORMED_BY | target -> source (the step is performed by the role) |
| WorkflowStep | BusinessRole | PERFORMED_BY | source -> target (the step is performed by the role) |
| BusinessRole | BusinessProcess | PARTICIPATES_IN | source -> target |
Decision nodes: Decision diamonds are stored in Neo4j as
WorkflowStepwithstereotype: "Решение"and astep_numberassigned in sequence alongside regular steps (see Phase 3.2). They have noPERFORMED_BYrelationship --- decisions are process gates, not role-assigned. In the table above, "Decision (diamond)" refers to Excalidraw diamond elements withcustomData.nodeType: "Decision"which becomeWorkflowStepnodes in Neo4j.
Special case for BusinessEntity arrows:
When an arrow goes from a WorkflowStep to a BusinessEntity, the relationship is PRODUCES (the step produces the entity).
When an arrow goes from a BusinessEntity to a WorkflowStep, the relationship is READS (the step reads/consumes the entity). In Neo4j, this is stored as (step)-[:READS]->(entity), so the Cypher direction is reversed from the arrow direction on the board.
Special case for BusinessRole arrows:
Regardless of arrow direction on the board, the Neo4j relationship is always (step)-[:PERFORMED_BY]->(role).
For each resolved arrow, use mcp__neo4j__write-cypher with the appropriate Cypher.
NEXT_STEP (WorkflowStep/Decision to WorkflowStep/Decision):
MATCH (source:WorkflowStep {id: $sourceId})
MATCH (target:WorkflowStep {id: $targetId})
MERGE (source)-[:NEXT_STEP]->(target)
RETURN source.id AS from, target.id AS to
PRODUCES (WorkflowStep to BusinessEntity):
MATCH (ws:WorkflowStep {id: $wsId})
MATCH (be:BusinessEntity {id: $entityId})
MERGE (ws)-[:PRODUCES]->(be)
RETURN ws.id AS step, be.id AS entity
READS (BusinessEntity arrow to WorkflowStep --- stored as step READS entity):
MATCH (ws:WorkflowStep {id: $wsId})
MATCH (be:BusinessEntity {id: $entityId})
MERGE (ws)-[:READS]->(be)
RETURN ws.id AS step, be.id AS entity
PERFORMED_BY (BusinessRole to/from WorkflowStep):
MATCH (ws:WorkflowStep {id: $wsId})
MATCH (r:BusinessRole {id: $roleId})
MERGE (ws)-[:PERFORMED_BY]->(r)
RETURN ws.id AS step, r.id AS role
After processing all explicit arrows, check for steps that lack a PERFORMED_BY relationship but are visually contained within a BusinessRole swimlane on the board.
For each WorkflowStep that has no PERFORMED_BY edge created in 4.3:
For each BusinessRole element on the board:
If step.x >= role.x AND step.y >= role.y
AND step.x + step.width <= role.x + role.width
AND step.y + step.height <= role.y + role.height:
-> The step is inside this role's swimlane
-> Create PERFORMED_BY relationship (same Cypher as 4.3)
[1/{total}] NEXT_STEP: BP-001-S01 -> BP-001-S02
[2/{total}] NEXT_STEP: BP-001-S02 -> BP-001-S03
[3/{total}] PRODUCES: BP-001-S03 -> OBJ-001
[4/{total}] READS: BP-001-S01 <- OBJ-002 (stored as BP-001-S01 -[:READS]-> OBJ-002)
[5/{total}] PERFORMED_BY: BP-001-S01 -> ROL-01
[6/{total}] PERFORMED_BY (swimlane): BP-001-S02 -> ROL-01
[7/{total}] Skipped arrow (id: arrow-015) --- dangling (endBinding: null)
Process every element in dirtySyncedElements[] (those with customData.nodeId != null AND customData.synced == false). These are elements that were previously synced but then modified on the board.
For each element, query the current node using mcp__neo4j__read-cypher:
MATCH (n {id: $nodeId})
RETURN n.id AS id,
labels(n) AS labels,
properties(n) AS props
If the node is not found in Neo4j, log an error:
[ERROR] Element '{label}' has nodeId={nodeId} but node not found in Neo4j. Skipping update.
Consider removing nodeId from customData and re-syncing as new element.
Skip this element.
Resolve the current board text via labelMap[element.id] and compare with the Neo4j node's name/function_name.
For WorkflowStep / Decision nodes (compare against function_name):
boardText = labelMap[element.id]
graphText = props.function_name
If boardText != graphText:
-> Update needed
Update via mcp__neo4j__write-cypher:
MATCH (ws:WorkflowStep {id: $wsId})
SET ws.function_name = $newFunctionName,
ws.updated = datetime()
RETURN ws.id AS id, ws.function_name AS function_name
For BusinessEntity nodes (compare against name):
MATCH (be:BusinessEntity {id: $entityId})
SET be.name = $newName,
be.updated = datetime()
RETURN be.id AS id, be.name AS name
For BusinessRole nodes (compare against full_name):
MATCH (r:BusinessRole {id: $roleId})
SET r.full_name = $newFullName,
r.updated = datetime()
RETURN r.id AS id, r.full_name AS full_name
After a successful update (or if the text has not changed), mark the element for sync confirmation in Phase 6:
element.customData.synced = true
[1/{total}] Updated WorkflowStep BP-001-S03: "Verify documents" -> "Verify and validate documents"
[2/{total}] No change for BusinessEntity OBJ-001: "Application form" (text unchanged)
[3/{total}] Updated BusinessRole ROL-02: "Manager" -> "Senior Manager"
[4/{total}] [ERROR] Node BP-001-S99 not found in Neo4j --- skipped
For each element that was successfully synced or created in Phases 3--5, update its customData:
{
"customData": {
"nodeId": "{assigned Neo4j ID}",
"nodeType": "{unchanged}",
"confidence": "{unchanged}",
"sourceDoc": "{unchanged}",
"sourcePage": "{unchanged}",
"synced": true
}
}
For every element where customData.synced was set to true in this run, change the strokeColor to green:
strokeColor = "#2e7d32"
This provides visual feedback that the element is confirmed in the graph.
Reconstruct the full .excalidraw JSON with the updated elements array and write it back to the same path using the Write tool.
Preserve all other fields (type, version, source, appState, files) unchanged. Preserve all elements that were not processed (arrows, text elements, skipped elements) exactly as they were.
After writing the board file, update the board meta sidecar. The canonical schema, hash algorithm, and merge rules are in ${CLAUDE_PLUGIN_ROOT}/nacl-core/SKILL.md § "Board Meta Sidecar (<board>.meta.json)".
Steps:
board_path: strip the directory prefix and the .excalidraw extension (e.g. {$boards_dir}/process-BP-001.excalidraw → process-BP-001).{$boards_dir}/<board>.meta.json and parse as JSON; if absent or unreadable, start from all-null defaults.computeBoardHash algorithm (see ${CLAUDE_PLUGIN_ROOT}/nacl-core/SKILL.md § "Board Meta Sidecar") to the scene that was just written. Strip volatile per-element keys (version, versionNonce, seed, updated), sort remaining element keys alphabetically, keep only viewBackgroundColor and gridSize from appState, serialize with JSON.stringify, SHA-256 the result, prefix with sha256:.lastGeneratedAt and lastGeneratedBy) unchanged:
lastSyncedAt = current UTC timestamp in ISO-8601 (e.g. "2026-05-03T19:00:00.000Z")lastSyncStatus = "ok"lastSyncRunId = the run ID passed in by the caller (e.g. analyst-tool's r-<hex> value), or null if the skill was invoked directly without a run IDcontentHashAtLastSync = the hash from step 3{$boards_dir}/<board>.meta.json.tmp, then rename to {$boards_dir}/<board>.meta.json.Run ID note: when invoked directly by a human via Claude Code (not via the analyst-tool's skill runner), there is no run ID. In that case set
lastSyncRunIdtonull. When invoked via the analyst-tool, the tool passes the run ID as context; use that value.
Print the report directly to the user:
## Sync Complete: {boardname}
**Board file:** `{board_path}`
**Sync date:** {YYYY-MM-DD HH:MM}
**Target process:** {targetBpId} ({bpName}) in {targetGprId} ({gprName})
---
### Nodes Created
| # | Neo4j ID | Type | Name |
|---|----------|------|------|
| 1 | BP-001-S01 | WorkflowStep | Review application |
| 2 | BP-001-S02 | WorkflowStep | Verify documents |
| 3 | OBJ-001 | BusinessEntity | Application form |
| 4 | ROL-01 | BusinessRole | Procurement Manager |
**Total created:** {N} ({X} WorkflowSteps, {Y} BusinessEntities, {Z} BusinessRoles)
---
### Relationships Created
| # | Type | From | To |
|---|------|------|----|
| 1 | NEXT_STEP | BP-001-S01 | BP-001-S02 |
| 2 | PRODUCES | BP-001-S02 | OBJ-001 |
| 3 | PERFORMED_BY | BP-001-S01 | ROL-01 |
**Total created:** {N} ({A} NEXT_STEP, {B} READS, {C} PRODUCES, {D} PERFORMED_BY)
---
### Nodes Updated
| # | Neo4j ID | Type | Old Value | New Value |
|---|----------|------|-----------|-----------|
| 1 | BP-001-S03 | WorkflowStep | Verify documents | Verify and validate documents |
**Total updated:** {N}
---
### Warnings & Errors
{List of all warnings and errors from all phases, or "None" if clean.}
---
### Board Visual Changes
- Elements with green stroke (#2e7d32): {N} (confirmed synced)
- Elements unchanged: {N}
---
### Next Steps
1. Run `/nacl:ba-analyze` to validate board completeness and graph consistency.
2. Elements with non-green stroke still need attention (medium/low confidence).
If there were errors, add:
3. Fix errors listed above and re-run `/nacl:ba-sync` for failed elements.
When sync fails partway through (validation error, Cypher write error, Neo4j connection lost, or conflict), the board file is NOT rewritten (per the error handling rules below). However, the meta sidecar must still be updated to record the failure so the analyst-tool can show the correct status.
On any terminal failure (after determining that the sync cannot proceed or did not complete cleanly):
board_path (same as 6.4 step 1).lastSyncStatus = "failed"lastSyncRunId = run ID if available, else nulllastSyncedAt or contentHashAtLastSync — these should still reflect the last successful sync..tmp then rename, same as the success path.This ensures the analyst-tool displays "last sync failed" rather than stale "ok" status.
This skill MUST be safe to run multiple times on the same board without creating duplicates.
All Neo4j node creation uses MERGE keyed on {id}. If a node with the same ID already exists, its properties are updated (ON MATCH SET) instead of creating a duplicate.
Elements where customData.synced == true AND customData.nodeId != null are in existingElements[]. They are not processed in Phase 3 or Phase 5 unless their synced flag has been reset to false (indicating a board edit after the last sync).
All relationship creation uses MERGE, not CREATE. Running the sync twice does not produce duplicate edges:
MERGE (source)-[:NEXT_STEP]->(target) // idempotent
MERGE (ws)-[:PERFORMED_BY]->(r) // idempotent
MERGE (ws)-[:READS]->(be) // idempotent
MERGE (ws)-[:PRODUCES]->(be) // idempotent
MERGE (gpr)-[:CONTAINS]->(bp) // idempotent
MERGE (bp)-[:HAS_STEP {order: N}]->(ws) // idempotent
MERGE (r)-[:PARTICIPATES_IN]->(bp) // idempotent
If an element already has customData.nodeId set (from a previous sync), that ID is reused. The skill never generates a new ID for an element that already has one.
Before creating a BusinessRole node, query for an existing role with the same full_name (case-insensitive). If found, reuse the existing node and skip creation (see Phase 3.4c).
Board file not found:
{path}. Verify the path or check{$boards_dir}/for available boards.
File
{path}is not a valid Excalidraw file. Expected JSON with"type": "excalidraw".
If any mcp__neo4j__write-cypher or mcp__neo4j__read-cypher call fails with a connection error:
Neo4j is not reachable at
bolt://localhost:{$neo4j_bolt_port}. Tell me "start the graph" and I will runnode "${CLAUDE_PLUGIN_ROOT}/nacl-core/scripts/graph-doctor.mjs" --fixvia Bash (works in Claude Code Desktop and CLI). Or start it yourself from the project root (main checkout, not a worktree):
- local mode:
docker compose -f graph-infra/docker-compose.yml up -d--- if Docker Desktop is not running, open the Docker Desktop app first- remote mode: relaunch the sidecar
~/.nacl/sidecar/<project_scope>.sh(Windows:%USERPROFILE%\.nacl\sidecar\<project_scope>.cmd) Cannot proceed with sync.
Stop execution. Do NOT write partial results to the board file --- the board must remain in its pre-sync state so that a retry produces correct results. Write the failure meta sidecar (see "Meta Sidecar on Failure" above).
If a single node creation fails but Neo4j is otherwise reachable:
customData (leave synced: false, nodeId: null)/nacl:ba-sync --- idempotency ensures already-synced elements are skippedWhen partial failures occur, Phase 6.4 still runs with lastSyncStatus = "ok" because the overall sync completed (failed individual elements are left for retry). If the majority of the sync failed or the board was not written, use lastSyncStatus = "failed" instead.
# Board file:
- {$boards_dir}/{boardname}.excalidraw # the board being synced
# Shared references:
- ${CLAUDE_PLUGIN_ROOT}/nacl-core/SKILL.md # ID formats, Excalidraw format, Neo4j schema, meta sidecar spec
# Neo4j (via MCP):
- mcp__neo4j__read-cypher # query existing nodes, generate next IDs
# Board file (updated with nodeId, synced, strokeColor):
- {$boards_dir}/{boardname}.excalidraw
# Meta sidecar (always written — success or failure):
- {$boards_dir}/{boardname}.meta.json
# Neo4j (via MCP):
- mcp__neo4j__write-cypher # create/update nodes, create relationships
None. The board file already exists (this skill reads an existing board).
| Tool | Purpose |
|---|---|
mcp__neo4j__read-cypher | Query existing nodes, generate next available IDs, read node state for comparison |
mcp__neo4j__write-cypher | Create/update nodes (MERGE), create relationships (MERGE) |
| Caller | Context |
|---|---|
| User | Manual invocation: /nacl:ba-sync [board_path] |
| Analyst Tool | Via itsalt-pinch skill runner; passes lastSyncRunId in run context |
| Recommended after | /nacl:ba-import-doc or /nacl:ba-from-board |
Before completing the sync, verify:
type: "excalidraw" confirmedcustomDatanpx claudepluginhub itsalt/nacl --plugin naclАнализ Excalidraw-доски бизнес-процесса: полнота, diff со snapshot, сравнение с Neo4j-графом. Используй когда пользователь просит: проанализировать доску, проверить доску, найти проблемы на доске, board analysis, nacl-ba-analyze.
Manages business process graphs, flowcharts, and actor-link structures in Simulator.Company. Supports creating, editing, querying nodes/edges and layers.
Creates and edits JSON Canvas (.canvas) files with nodes, edges, and groups for visual diagrams, mind maps, and flowcharts in Obsidian and other apps.