{"slug":"schedule-extractor","title":"schedule-extractor","summary":"Extract tabular schedule data from construction drawings — door, window, finish, fixture, panel schedules — and output to Excel. Triggers: 'door schedule', 'extract schedule', 'schedule to Excel', 'panel schedule'.","platform":"Claude","tags":[],"authorName":"LLM Mart","authorSlug":"llm-mart","score":0,"source":"github","price":null,"verified":false,"createdAt":"2026-10-19T21:17:03.319735Z","repo":{"url":"https://github.com/dleerdefi/claude-code-construction","stars":44,"forks":13,"license":"MIT","updatedAt":"2026-10-18T06:12:41Z"},"bodyHtml":"<hr>\n<h2>name: schedule-extractor\ndescription: &gt;\nExtract tabular schedule data from construction drawings — door, window,\nfinish, fixture, panel schedules — and output to Excel. Triggers: 'door\nschedule', 'extract schedule', 'schedule to Excel', 'panel schedule'.\nargument-hint: \"&lt;sheet_number&gt; [schedule_type]\"\ndisable-model-invocation: true</h2>\n<h1>Schedule Extractor</h1>\n<p>Extracts tabular schedule data embedded in drawing sheets or spec pages and outputs structured Excel files. Schedules are typically one element among many on a sheet — or the entire sheet may be a schedule.</p>\n<h2>First: the mode (both workflows)</h2>\n<p>Before extracting or reconciling, read <code>.construction/project.yaml</code>. Present and readable means AgentCM mode: note its <code>contract_version</code> and follow <code>${CLAUDE_PLUGIN_ROOT}/reference/agentcm-mode.md</code>. Absent means flat-file mode. A listing that shows the file, or <code>.construction/database.yaml</code> alone, does not decide it: read the file.</p>\n<h2>Workflow</h2>\n<pre><code>Extraction Progress:\n- [ ] Step 1: Discover schedule locations\n- [ ] Step 2: Isolate (crop) the schedule region\n- [ ] Step 3: Extract structured data (pdfplumber + vision)\n- [ ] Step 4: Validate and clean data\n- [ ] Step 5: Output to Excel\n- [ ] Step 6: Ingest to AgentCM (AgentCM mode)\n- [ ] Step 7: Write a finding\n</code></pre>\n<h3>Step 1: Discover Schedule Locations</h3>\n<p>Use a multi-source discovery approach, checking all available sources:</p>\n<h4>Source A — Sheet titles in sheet index (primary, most reliable)</h4>\n<p>Search <code>.construction/index/sheet_index.yaml</code> (or the sheet index you've built) for sheets with \"SCHEDULE\" in the title. Many important schedules occupy an <strong>entire sheet</strong> — the sheet title tells you exactly what it is:</p>\n<ul>\n<li>\"DOOR SCHEDULE\" → entire sheet is a door schedule</li>\n<li>\"FINISH SCHEDULE\" or \"ROOM FINISH SCHEDULE\" → entire sheet is finishes</li>\n<li>\"WINDOW SCHEDULE\" → entire sheet is windows</li>\n<li>\"PANEL SCHEDULE\" → electrical panel schedule</li>\n<li>\"FIXTURE SCHEDULE\" → plumbing fixtures</li>\n</ul>\n<p>For dedicated schedule sheets, the entire page is the extraction target — no need to crop.</p>\n<h4>Source B — AgentCM's tables (AgentCM mode)</h4>\n<p>In AgentCM mode (<code>.construction/project.yaml</code> exists; follow <code>${CLAUDE_PLUGIN_ROOT}/reference/agentcm-mode.md</code>), <code>.construction/INDEX.md</code>'s Schedules and legends table lists every table AgentCM found, each in its own <code>.construction/schedules/&lt;sheet&gt;-&lt;n&gt;.tsv</code>. The file's comment line gives the table's box on the sheet (x y w h, 0 to 1 from the top left) and, for a schedule already ingested, its <code>schedule_id</code> (<code>none</code> for a table read from the sheet's text alone). For embedded schedules on non-schedule sheets, that box is where to look: <code>sh .construction/crop.sh &lt;sheet&gt; &lt;x&gt; &lt;y&gt; &lt;w&gt; &lt;h&gt;</code>.</p>\n<p><strong>Note</strong>: A table read from the text is AgentCM's reading, and its box an estimate: treat both as leads, not definitive boundaries. Always verify with vision.</p>\n<h4>Source C — Discipline-based heuristics</h4>\n<p>Schedules appear on specific sheet types:</p>\n<ul>\n<li><strong>Door schedule</strong> → typically on A-0.XX or A-8.XX sheets, or a dedicated sheet</li>\n<li><strong>Window schedule</strong> → same sheets as door schedule, or separate</li>\n<li><strong>Room finish schedule</strong> → A-0.XX or interior sheets</li>\n<li><strong>Panel schedule</strong> → E-X.XX electrical sheets</li>\n<li><strong>Fixture schedule</strong> → P-X.XX plumbing sheets</li>\n<li><strong>Equipment schedule</strong> → M-X.XX mechanical sheets</li>\n</ul>\n<h4>Source D — Vision scan (fallback)</h4>\n<p>If no index or graph is available:</p>\n<pre><code>\"${CLAUDE_PLUGIN_ROOT}/bin/construction-python\" \"${CLAUDE_PLUGIN_ROOT}/scripts/pdf/rasterize_page.py\" \"{pdf_path}\" {page} --dpi 150 --output \".construction/skills/schedule-extractor/pages/full_sheet.png\"\n</code></pre>\n<p>Use vision on the full sheet image: \"Identify any tabular schedules on this drawing sheet. Report the approximate bounding box (top-left x,y and bottom-right x,y) as fractions of the image width and height between 0 and 1, the schedule type, and the column headers visible.\"</p>\n<h3>Step 2: Isolate the Schedule Region</h3>\n<p><strong>For dedicated schedule sheets</strong> (entire page is a schedule): Skip cropping — use the full page.</p>\n<p><strong>For embedded schedules</strong> (schedule is one element on a larger sheet):</p>\n<p>Crop the identified region with padding. The box is the 0–1 fractions from\nStep 1, so <code>--normalized</code> is required; without it the script reads the values\nas pixels and returns an empty crop:</p>\n<pre><code>\"${CLAUDE_PLUGIN_ROOT}/bin/construction-python\" \"${CLAUDE_PLUGIN_ROOT}/scripts/pdf/crop_region.py\" \".construction/skills/schedule-extractor/pages/full_sheet.png\" \\\n  --box {x1},{y1},{x2},{y2} --normalized \\\n  --padding 20 \\\n  --output \".construction/skills/schedule-extractor/pages/schedule_crop.png\"\n</code></pre>\n<p>Re-rasterize at higher DPI (300) for the cropped region to improve text clarity.\n<code>rasterize_page.py --crop</code> takes percentages, so multiply each fraction by 100\n(<code>0.62,0.08,0.98,0.45</code> becomes <code>62,8,98,45</code>):</p>\n<pre><code>\"${CLAUDE_PLUGIN_ROOT}/bin/construction-python\" \"${CLAUDE_PLUGIN_ROOT}/scripts/pdf/rasterize_page.py\" \"{pdf_path}\" {page} \\\n  --dpi 300 \\\n  --crop {x1_pct},{y1_pct},{x2_pct},{y2_pct} \\\n  --output \".construction/skills/schedule-extractor/pages/schedule_hires.png\"\n</code></pre>\n<h3>Step 3: Extract Structured Data</h3>\n<h4>In AgentCM mode — the table AgentCM read</h4>\n<p>A table INDEX.md lists under Schedules and legends is already read. Turn its file into the exporter's data with the reader, never with a converter of your own:</p>\n<pre><code>\"${CLAUDE_PLUGIN_ROOT}/bin/construction-python\" \"${CLAUDE_SKILL_DIR}/scripts/agentcm_schedule.py\" \".construction/schedules/&lt;sheet&gt;-&lt;n&gt;.tsv\" \\\n  --type door_schedule --data \".construction/skills/schedule-extractor/&lt;sheet&gt;-&lt;n&gt;.json\"\n</code></pre>\n<p>It prints the header row it took and what it left out. A table read from the sheet's text has its title and header rows at the top: when it took the wrong row, give <code>--header-row &lt;y&gt;</code> (the line's position, as the file prints it) and <code>--header-rows &lt;n&gt;</code> for a header on several lines. Then verify the table against the drawing with <code>crop.sh</code> and the box on the file's comment line (Source B). Where the drawing shows a different value, correct that cell in the data JSON with the Edit tool, and record the correction as <code>${CLAUDE_PLUGIN_ROOT}/reference/agentcm-mode.md</code> says. Methods A to C below are for a table AgentCM did not find.</p>\n<p>Otherwise use a <strong>try → validate → fallback</strong> approach:</p>\n<h4>Method A — pdfplumber table extraction (try first)</h4>\n<p>Extract the page's tables with the shared script (its JSON holds <code>text</code> and <code>tables</code>; add <code>--bbox x1,y1,x2,y2</code> in PDF points to limit it to the schedule region):</p>\n<pre><code>\"${CLAUDE_PLUGIN_ROOT}/bin/construction-python\" \"${CLAUDE_PLUGIN_ROOT}/scripts/pdf/extract_text_region.py\" \"{pdf_path}\" {page} --output \".construction/skills/schedule-extractor/tables.json\"\n</code></pre>\n<p>If multiple tables are found, select the largest one (most rows with the most columns — schedules are wide). The first row of the selected table contains headers; subsequent rows are data. If no tables are found or results look garbled, fall back to vision (Method B). Do not write pdfplumber code of your own.</p>\n<h4>Evaluate Method A — Quality Gate</h4>\n<p><strong>Check these criteria before proceeding:</strong></p>\n<ol>\n<li><strong>Row count</strong>: Did pdfplumber extract at least 10 data rows? Most schedules have 20-200+ entries.</li>\n<li><strong>Column consistency</strong>: Do ≥80% of rows have the same number of columns? Inconsistent columns = parsing error.</li>\n<li><strong>Non-empty cells</strong>: Are &gt;50% of cells non-empty? Mostly-empty extraction = parsing failed.</li>\n<li><strong>First column validity</strong>: Does the first column contain recognizable IDs (door numbers, room numbers, equipment tags)?</li>\n</ol>\n<p><strong>Decision:</strong></p>\n<ul>\n<li>All 4 checks pass → <strong>Method A succeeded</strong>, proceed to Step 4</li>\n<li>Any check fails → <strong>Method A failed</strong>, use Method B below</li>\n</ul>\n<pre><code>QUALITY GATE:\n  extracted_rows &gt;= 10?          YES → check next  |  NO → use Method B\n  column_consistency &gt;= 80%?     YES → check next  |  NO → use Method B\n  non_empty_cells &gt;= 50%?        YES → check next  |  NO → use Method B\n  first_col_has_valid_ids?       YES → use Method A |  NO → use Method B\n</code></pre>\n<h4>Method B — Vision extraction (fallback)</h4>\n<p>When pdfplumber fails (common with complex layouts, merged cells, non-standard table lines):</p>\n<ol>\n<li><strong>Rasterize</strong> the sheet at 150 DPI for overview:</li>\n</ol>\n<pre><code>\"${CLAUDE_PLUGIN_ROOT}/bin/construction-python\" \"${CLAUDE_PLUGIN_ROOT}/scripts/pdf/rasterize_page.py\" \"{pdf_path}\" {page} --dpi 150 --output \".construction/skills/schedule-extractor/pages/full_sheet.png\"\n</code></pre>\n<ol start=\"2\">\n<li><strong>Use vision</strong> on the full sheet image to read the schedule directly:</li>\n</ol>\n<p>\"This is a construction drawing sheet containing a schedule (tabular data). Extract ALL rows from the schedule as a JSON array of objects. The first row of the table contains column headers — use those as object keys. Each subsequent row is one entry. Preserve exact values including dimensions, abbreviations, and codes. If a cell is empty, use null.\"</p>\n<ol start=\"3\">\n<li>For dense schedules with small text, <strong>re-rasterize at 300 DPI</strong> and crop to just the schedule region:</li>\n</ol>\n<pre><code>\"${CLAUDE_PLUGIN_ROOT}/bin/construction-python\" \"${CLAUDE_PLUGIN_ROOT}/scripts/pdf/rasterize_page.py\" \"{pdf_path}\" {page} --dpi 300 --output \".construction/skills/schedule-extractor/pages/schedule_hires.png\"\n</code></pre>\n<h4>Method C — Hybrid (for maximum accuracy)</h4>\n<p>Run both methods and cross-validate:</p>\n<ul>\n<li>pdfplumber gives exact text positioning and catches every character</li>\n<li>Vision catches content pdfplumber misses (stamps, handwritten marks, non-standard table layouts)</li>\n<li>Compare row counts: if they differ by &gt;10%, investigate the discrepancy</li>\n<li>Use vision results for rows that pdfplumber returned empty or garbled</li>\n</ul>\n<h3>Step 4: Validate and Clean</h3>\n<p>After extraction (by either method):</p>\n<ul>\n<li><strong>Row count check</strong>: Compare extracted count against what's visually present. If off by &gt;10%, re-extract using the other method.</li>\n<li><strong>Column structure</strong>: Verify headers match expected schedule type (door schedule has MARK/SIZE/TYPE/FRAME/HARDWARE; finish schedule has RM.NO/NAME/FLOOR/BASE/WALLS/CLG)</li>\n<li><strong>Merged cell cleanup</strong>: Expand merged header cells (e.g., \"WALLS\" spanning A/B/C/D sub-columns)</li>\n<li><strong>Normalize dimensions</strong>: <code>3' - 0\"</code> → <code>3'-0\"</code> (remove spaces around dashes)</li>\n<li><strong>Flag revisions</strong>: Note any cells within revision clouds or delta markers</li>\n<li><strong>Cross-reference with AgentCM's identifiers</strong>: In AgentCM mode, check each room number and door mark against <code>.construction/tags.tsv</code> (<code>kind</code> room or door; the lines starting with the number and a tab). Not finding one there proves nothing (INDEX.md says when its room list is incomplete): report it as \"not in AgentCM's index\" and check the plan sheets' <code>.construction/sheets/&lt;sheet&gt;.tsv</code>.</li>\n</ul>\n<h3>Step 5: Output to Excel</h3>\n<pre><code>\"${CLAUDE_PLUGIN_ROOT}/bin/construction-python\" \"${CLAUDE_SKILL_DIR}/scripts/schedule_to_xlsx.py\" \\\n  --data \".construction/skills/schedule-extractor/&lt;sheet&gt;-&lt;n&gt;.json\" \\\n  --type door_schedule \\\n  --project \"Project Name\" \\\n  --sheet \"A-0.01\" \\\n  --output \"Door_Schedule_A-0.01.xlsx\"\n</code></pre>\n<p>The data JSON is working data: write it (with the Write tool, or the reader in AgentCM mode) under <code>.construction/skills/schedule-extractor/</code>, never in the project root or a folder of your own. The workbook is the deliverable: write it in the project folder as the command above shows, never under <code>.construction/</code>. For a table AgentCM has already ingested (<code>schedule_id=&lt;id&gt;</code> on its file's comment line), add <code>--schedule-id &lt;id&gt; --sheet-id &lt;the sheet's id&gt;</code> so the workbook can come back through the round trip.</p>\n<p>The script creates a formatted Excel workbook with:</p>\n<ul>\n<li>Header row with project info and source sheet</li>\n<li>Auto-sized columns</li>\n<li>Conditional formatting for empty/flagged cells</li>\n<li>Source metadata in a separate tab</li>\n</ul>\n<h3>Step 6: Ingest to AgentCM (AgentCM mode)</h3>\n<p>After Excel output, send the extracted data to AgentCM's schedule ingest, which stores it and links its rows to rooms. The payload (below) goes in <code>.construction/skills/schedule-extractor/outbox/schedules-ingest-&lt;sheet&gt;-&lt;YYYYMMDD-HHMMSS&gt;.json</code>:</p>\n<ul>\n<li><strong>A table from <code>schedules/</code> not yet ingested</strong> (<code>schedule_id=none</code>): the reader writes it. Run it again with <code>--payload &lt;that path&gt; --sheet-id &lt;id&gt; --row-entity-type &lt;type&gt; --source-method hybrid --confidence &lt;0 to 1&gt;</code> (<code>hybrid</code>: AgentCM's reading, checked by eye with <code>crop.sh</code>), and make any correction from Step 3 in the payload's cell too.</li>\n<li><strong>A table already ingested</strong> (<code>schedule_id=&lt;id&gt;</code>): nothing to ingest. The reader refuses, and the PE edits come back through the round trip below.</li>\n<li><strong>A table read by Methods A to C:</strong> write the payload with the Write tool. <code>sourceMethod</code> is one of <code>pdfplumber</code>, <code>vision</code>, <code>hybrid</code> or <code>manual</code>, and <code>rowEntityType</code> one of the types listed below.</li>\n</ul>\n<p>Send it as <code>${CLAUDE_PLUGIN_ROOT}/reference/agentcm-mode.md</code> says under Writing back; if AgentCM is closed, the payload waits there and the PE is told:</p>\n<pre><code># api_url and project_id from .construction/database.yaml\ncurl -sS -X POST \"{api_url}/projects/{project_id}/schedules/ingest\" \\\n  -H \"Content-Type: application/json\" \\\n  -d @\".construction/skills/schedule-extractor/outbox/schedules-ingest-&lt;sheet&gt;-&lt;YYYYMMDD-HHMMSS&gt;.json\" \\\n  -w \"\\nHTTP %{http_code}\\n\"\n</code></pre>\n<p><strong>The sheet's id.</strong> <code>sheetId</code> is the <code>id</code> of the entry in <code>.construction/index/sheet_index.yaml</code> whose <code>sheet_number</code> is the schedule's sheet. Never guess it from the number. If that file is missing, or its entries have no <code>id</code> (an older sheet-splitter wrote a <code>pages:</code> list with <code>filename</code> and <code>page_index</code>), say so and stop: do not ingest. The Excel workbook from Step 5 stands either way.</p>\n<p>The ingest payload JSON follows this structure:</p>\n<pre><code>{\n  \"sheetId\": \"&lt;uuid of the source sheet&gt;\",\n  \"scheduleType\": \"door\",\n  \"title\": \"DOOR SCHEDULE\",\n  \"rowEntityType\": \"door\",\n  \"columns\": [\n    {\"key\": \"MARK\", \"header\": \"MARK\"},\n    {\"key\": \"WIDTH\", \"header\": \"WIDTH\"},\n    {\"key\": \"HEIGHT\", \"header\": \"HEIGHT\"},\n    {\"key\": \"TYPE\", \"header\": \"TYPE\"},\n    {\"key\": \"FRAME\", \"header\": \"FRAME\"},\n    {\"key\": \"HARDWARE_SET\", \"header\": \"HARDWARE SET\"},\n    {\"key\": \"FIRE_RATING\", \"header\": \"FIRE RATING\"}\n  ],\n  \"rows\": [\n    {\n      \"entityIdentifier\": \"2.101A\",\n      \"cells\": {\"MARK\": \"2.101A\", \"WIDTH\": \"3'-0\\\"\", \"HEIGHT\": \"7'-0\\\"\", \"TYPE\": \"A\", \"FRAME\": \"HM\", \"HARDWARE_SET\": \"HW-3\", \"FIRE_RATING\": \"1 HR\"}\n    }\n  ],\n  \"sourceMethod\": \"pdfplumber\",\n  \"confidence\": 0.95\n}\n</code></pre>\n<p><strong>Schedule types and their <code>rowEntityType</code>:</strong></p>\n<ul>\n<li>Door schedule → <code>\"door\"</code> (room derived from door number: \"2.101A\" → Room 2.101)</li>\n<li>Window schedule → <code>\"window\"</code> (room derived from window mark)</li>\n<li>Finish schedule → <code>\"room\"</code> (entity identifier IS the room number)</li>\n<li>Panel schedule → <code>\"equipment\"</code></li>\n<li>Fixture schedule → <code>\"fixture\"</code> (room linked spatially from plan tags)</li>\n</ul>\n<p>The ingest endpoint automatically:</p>\n<ol>\n<li>Creates/updates the schedule record</li>\n<li>Inserts all rows with GIN-indexed jsonb cells</li>\n<li>Links rows to rooms (exact match, fuzzy match, or creates rooms from schedule data)</li>\n<li>Extracts finish/hardware codes into the code legends table</li>\n<li>Creates pending links for fixtures/equipment that need spatial resolution</li>\n</ol>\n<h3>Step 7: Write a Finding</h3>\n<p>In both modes, record the extraction in <code>.construction/skills/schedule-extractor/findings/</code>:</p>\n<pre><code>\"${CLAUDE_PLUGIN_ROOT}/bin/construction-python\" \"${CLAUDE_PLUGIN_ROOT}/scripts/graph/write_finding.py\" \\\n  --skill \"schedule-extractor\" --type \"schedule_extracted\" \\\n  --title \"Door schedule extracted from A-0.01\" \\\n  --source-sheet \"A-0.01\" \\\n  --output-file \"Door_Schedule_A-0.01.xlsx\" \\\n  --data '{\"schedule_type\": \"door\", \"row_count\": 45, \"columns\": [\"MARK\",\"SIZE\",\"TYPE\",\"FRAME\",\"HARDWARE SET\"]}'\n</code></pre>\n<h2>Schedule Reconciliation (Excel → DB Round-Trip)</h2>\n<p>When a PE edits an exported Excel schedule and wants changes applied back to AgentCM:</p>\n<h3>Reconciliation Workflow</h3>\n<pre><code>Reconciliation Progress:\n- [ ] Step R1: Read metadata from edited Excel\n- [ ] Step R2: Compute changeset (diff vs DB)\n- [ ] Step R3: POST changeset to reconcile endpoint\n- [ ] Step R4: Report summary to user\n</code></pre>\n<h3>Step R1: Read Metadata from Edited Excel</h3>\n<p>Exported Excel files contain reconciliation anchors:</p>\n<ul>\n<li>Hidden <code>_agentcm_meta</code> sheet with <code>schedule_id</code>, <code>schedule_type</code>, <code>sheet_id</code></li>\n<li>Hidden <code>_row_key</code> column A with entity identifiers (door numbers, room numbers, etc.)</li>\n</ul>\n<p>If the Excel lacks <code>_agentcm_meta</code>, it was not exported by AgentCM — treat as a fresh import (re-run the extraction workflow above).</p>\n<h3>Step R2: Compute Changeset</h3>\n<p>Run the diff engine to compare the edited Excel against the rows AgentCM holds:</p>\n<pre><code>\"${CLAUDE_PLUGIN_ROOT}/bin/construction-python\" \"${CLAUDE_SKILL_DIR}/scripts/xlsx_to_changeset.py\" \\\n  --excel \"edited_schedule.xlsx\" \\\n  --output \".construction/skills/schedule-extractor/outbox/schedules-reconcile-&lt;sheet&gt;-&lt;YYYYMMDD-HHMMSS&gt;.json\"\n</code></pre>\n<p>The script:</p>\n<ol>\n<li>Reads <code>_agentcm_meta</code> → extracts <code>schedule_id</code>. A table never ingested (<code>schedule_id=none</code>) has nothing to reconcile and is refused: ingest it first (Step 6)</li>\n<li>Reads data sheet → extracts <code>_row_key</code> column + all data columns</li>\n<li>Reads the schedule's stored rows from AgentCM, <code>GET {api_url}/projects/{project_id}/schedules/{schedule_id}/rows</code>, with <code>project_id</code> and <code>api_url</code> from <code>.construction/database.yaml</code> (nothing else in it)</li>\n<li>Computes diff: cell changes, new rows, deleted rows, new columns, hidden columns</li>\n<li>Outputs a structured JSON changeset, with <code>sourceFile</code> the edited Excel's name</li>\n</ol>\n<p>It needs AgentCM running. If AgentCM does not answer, the script exits 3 and computes nothing: tell the PE to start AgentCM and rerun this step (the edited Excel is all it needs).</p>\n<h3>Step R3: POST Changeset to Reconcile Endpoint</h3>\n<p>Send the changeset R2 wrote to the outbox, as <code>${CLAUDE_PLUGIN_ROOT}/reference/agentcm-mode.md</code> says under Writing back:</p>\n<pre><code># api_url and project_id from .construction/database.yaml\ncurl -sS -X POST \"{api_url}/projects/{project_id}/schedules/reconcile\" \\\n  -H \"Content-Type: application/json\" \\\n  -d @\".construction/skills/schedule-extractor/outbox/schedules-reconcile-&lt;sheet&gt;-&lt;YYYYMMDD-HHMMSS&gt;.json\" \\\n  -w \"\\nHTTP %{http_code}\\n\"\n</code></pre>\n<p>The reconcile endpoint applies changes with:</p>\n<ul>\n<li><strong>Override protection</strong>: All user edits set <code>isUserOverride=TRUE</code> — future re-extractions will not overwrite them</li>\n<li><strong>Audit trail</strong>: Every cell edit, row add/delete, and column change is logged to <code>schedule_change_log</code></li>\n<li><strong>Conflict detection</strong>: If a re-extraction later disagrees with a user override, a conflict is created (not silently overwritten)</li>\n</ul>\n<h3>Step R4: Report Summary</h3>\n<p>The endpoint returns an <code>ExcelReconcileResult</code>:</p>\n<pre><code>{\n  \"rowsUpdated\": 3,\n  \"rowsAdded\": 1,\n  \"rowsSoftDeleted\": 0,\n  \"cellsChanged\": 7,\n  \"conflictsCreated\": 0,\n  \"columnsAdded\": 0,\n  \"columnsHidden\": 0,\n  \"errors\": []\n}\n</code></pre>\n<p>Report the summary to the user. If <code>errors</code> is non-empty, surface those for review.</p>\n<h3>Anchored Excel Export</h3>\n<p>When exporting schedules for PE review, always include reconciliation anchors:</p>\n<pre><code>\"${CLAUDE_PLUGIN_ROOT}/bin/construction-python\" \"${CLAUDE_SKILL_DIR}/scripts/schedule_to_xlsx.py\" \\\n  --data schedule_data.json \\\n  --type door_schedule \\\n  --project \"Project Name\" \\\n  --sheet \"A-0.01\" \\\n  --schedule-id \"{schedule_uuid}\" \\\n  --sheet-id \"{sheet_uuid}\" \\\n  --output \"Door_Schedule_A-0.01.xlsx\"\n</code></pre>\n<p>The <code>--schedule-id</code> and <code>--sheet-id</code> flags embed metadata in the hidden <code>_agentcm_meta</code> sheet, enabling the round-trip reconciliation workflow.</p>\n<h2>Schedule Type Reference</h2>\n<table>\n<thead>\n<tr>\n<th>Type</th>\n<th>Common columns</th>\n<th>Notes</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Door</td>\n<td>Mark, Width, Height, Type, Frame, Hardware Set, Fire Rating</td>\n<td>Cross-ref hardware sets with spec 08 71 00</td>\n</tr>\n<tr>\n<td>Window</td>\n<td>Mark, Width, Height, Type, Glazing, Frame Material</td>\n<td>Check for energy code compliance</td>\n</tr>\n<tr>\n<td>Room Finish</td>\n<td>Room #, Name, Floor, Base, North/South/East/West Walls, Ceiling</td>\n<td>Finish codes defined in legend</td>\n</tr>\n<tr>\n<td>Panel</td>\n<td>Circuit #, Description, Load (VA), Breaker Size, Phase</td>\n<td>Verify total load vs panel capacity</td>\n</tr>\n<tr>\n<td>Fixture (Plumbing)</td>\n<td>Mark, Description, Manufacturer, Model, Connection</td>\n<td>Cross-ref with spec Division 22</td>\n</tr>\n<tr>\n<td>Equipment (HVAC)</td>\n<td>Tag, Description, CFM/BTU, Voltage, Weight</td>\n<td>Cross-ref with spec Division 23</td>\n</tr>\n</tbody>\n</table>\n<h2>File Safety</h2>\n<p>Never overwrite an existing schedule extraction. The export script uses <code>safe_output_path()</code> which appends <code>_v2</code>, <code>_v3</code>, etc. automatically.</p>\n<hr>\n<h2>Allowed Scripts</h2>\n<p><strong>Allowed scripts — exhaustive list.</strong> Only execute these scripts during this skill:</p>\n<ul>\n<li><code>${CLAUDE_PLUGIN_ROOT}/scripts/pdf/extract_text_region.py</code> — Method A table extraction (pdfplumber)</li>\n<li><code>${CLAUDE_PLUGIN_ROOT}/scripts/pdf/rasterize_page.py</code> — rasterize PDF pages for vision extraction</li>\n<li><code>${CLAUDE_PLUGIN_ROOT}/scripts/pdf/crop_region.py</code> — crop schedule region from full sheet image</li>\n<li><code>scripts/agentcm_schedule.py</code> — AgentCM mode: a table from <code>.construction/schedules/</code> to the exporter's data and, for one not yet ingested, the ingest payload (Steps 3 and 6)</li>\n<li><code>scripts/schedule_to_xlsx.py</code> — Excel export with reconciliation anchors</li>\n<li><code>scripts/xlsx_to_changeset.py</code> — diff the edited Excel against AgentCM's stored rows (its rows endpoint) for reconciliation</li>\n<li><code>${CLAUDE_PLUGIN_ROOT}/scripts/graph/write_finding.py</code> — record the extraction as a finding (Step 7)</li>\n<li><code>${CLAUDE_PLUGIN_ROOT}/scripts/corrections_append.py</code> — AgentCM mode: record a correction of AgentCM's data (<code>reference/agentcm-mode.md</code>)</li>\n</ul>\n","files":[{"path":"agents/openai.yaml","sizeBytes":268,"isText":true},{"path":"scripts/agentcm_schedule.py","sizeBytes":9675,"isText":true},{"path":"scripts/schedule_to_xlsx.py","sizeBytes":7815,"isText":true},{"path":"scripts/shared.py","sizeBytes":554,"isText":true},{"path":"scripts/xlsx_to_changeset.py","sizeBytes":12863,"isText":true},{"path":"SKILL.md","sizeBytes":21256,"isText":true}],"reviewScore":null,"reviewSummary":null,"trust":{"provenance":"trusted-source-unreviewed","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow.","bodySource":null},"bodyLocked":false,"purchaseUrl":null,"sourceUrl":null,"report":{"provenance":"trusted-source-unreviewed","screen":{"ran":true,"outcome":"clean","suspicious":0,"notes":0,"hiddenCharacters":false},"virusScan":{"engine":"clamav","status":"clean","scannedAt":"2026-10-19T21:17:14.158065Z","sha256":"BE3C1A36D3DB3B57DF8CDFC1E2C49CBC1E07A872FB4270D3062C476F8753C25C","sizeBytes":19169},"review":null,"source":{"repositoryUrl":"https://github.com/dleerdefi/claude-code-construction","path":"skills/schedule-extractor","license":"MIT","commit":"5c46e16c67cdc82ee8bcbb0fdd8ba316d792d4ec","subtreeSha":"2FE08600BB48767D29735E2DB52D92984D3D2A098870E3E8EE20065C286EEF2F","lastSyncedAt":"2026-10-19T21:17:01.926806Z"},"reviewedAt":"2026-10-19T21:17:40.350764Z","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow."},"install":[{"target":"skills-cli","command":"npx skills add https://github.com/dleerdefi/claude-code-construction/tree/main/skills/schedule-extractor"},{"target":"claude-code","command":"claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install dleerdefi-claude-code-construction@llmmart"},{"target":"git","command":"git clone https://github.com/dleerdefi/claude-code-construction.git"}]}