{
  "openapi": "3.0.3",
  "info": {
    "title": "Pezley Portal Ops API",
    "version": "v2133",
    "description": "HTTP API for Portal Ops — external assistants and automations authenticated with org-issued `ppops_…` keys. Primary endpoint: `POST /.netlify/functions/portal-ops-mrz-tool`. Legacy alias `POST /.netlify/functions/openclaw-mrz-tool` (deprecated re-export). Issue keys in the Pezley Portal app under Settings → Portal Ops keys. Estimates — PRICING RULE: price painting and staining through areas[] (substrates with productId, productionRateId, coats) so the crew work order gets hours and materials. Use a flat-priced lineItem only for an add-on such as lift or equipment rental (set flatAddOn:true). Never add a separate paint/stain materials line; materials come from the products on area substrates. GOOD: areas:[{name:\"Living room\",length:18,width:15,height:9,substrates:[{name:\"Walls\",productionRateId,productId,coats:2}]}] plus lineItems:[{name:\"Boom lift rental\",amount:1200,flatAddOn:true}]. BAD: {name:\"Exterior painting\",amount:4800} (painting as a flat price, so no crew hours); {name:\"Paint and materials\",amount:650} or {name:\"Duration (5 gal)\",amount:450} (separate materials line). `unknown-field` errors include `allowedFields`. Remove draft lines with `removeLineItemIds` (idempotent).",
    "contact": {
      "name": "Pezley Portal",
      "url": "https://pezleyportal.com"
    }
  },
  "servers": [
    {
      "url": "https://pezleyportal.com",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "portal-ops",
      "description": "Portal Ops API (ops key or admin Bearer)"
    },
    {
      "name": "portal-ops-keys",
      "description": "Issue and manage ops keys (Firebase admin Bearer only)"
    },
    {
      "name": "briefing-read",
      "description": "Morning brief & reads"
    },
    {
      "name": "action-feed",
      "description": "Tasks & alerts (medium risk)"
    },
    {
      "name": "company-state",
      "description": "Company memory & inventory (medium risk)"
    },
    {
      "name": "leads-crm",
      "description": "Leads & pipeline"
    },
    {
      "name": "proposals-estimates",
      "description": "Proposals & estimates (high risk)"
    },
    {
      "name": "jobs-files",
      "description": "Jobs & files (high risk)"
    },
    {
      "name": "home-file",
      "description": "Home File (high risk)"
    },
    {
      "name": "scheduling",
      "description": "Scheduling & visits (medium risk)"
    },
    {
      "name": "job-delays",
      "description": "Job delay posts (medium risk)"
    },
    {
      "name": "job-status",
      "description": "Job status & limbo (medium risk)"
    },
    {
      "name": "crew-clock",
      "description": "Crew & time clock (high risk)"
    },
    {
      "name": "production-rates",
      "description": "Production rates"
    },
    {
      "name": "drips-comms",
      "description": "Email & drips"
    },
    {
      "name": "business-data",
      "description": "Business data (read)"
    },
    {
      "name": "photos",
      "description": "Photos (upload & annotate) (medium risk)"
    },
    {
      "name": "webhooks",
      "description": "Event webhooks (medium risk)"
    },
    {
      "name": "owner-finance",
      "description": "Owner $ — Numbers & job costing (high risk)"
    },
    {
      "name": "app-import",
      "description": "Import from another app (high risk)"
    }
  ],
  "x-portal-ops-scope-groups": [
    {
      "id": "briefing-read",
      "label": "Morning brief & reads",
      "description": "Jobs, crew, clock scans, learning AI, recurring reminders, check-ins. Home File, including gate codes, is a separate Home File grant.",
      "risk": null,
      "toolCount": 9
    },
    {
      "id": "action-feed",
      "label": "Tasks & alerts",
      "description": "Full Action Feed cleanup for this org: list/approve pending check-ins, one-off reminders — same mutate surface as Today (not read-only).",
      "risk": "medium",
      "toolCount": 3
    },
    {
      "id": "company-state",
      "label": "Company memory & inventory",
      "description": "Company profile, lore (remember/forget), tools checkout, reminders.",
      "risk": "medium",
      "toolCount": 15
    },
    {
      "id": "leads-crm",
      "label": "Leads & pipeline",
      "description": "list_leads and get_lead return full website CRM records and linked consent docs (including phone numbers). Pipeline scoring, follow-up drafts, and create_lead. list_sales_leads is Sales hub to-build jobs, not these CRM records.",
      "risk": null,
      "toolCount": 7
    },
    {
      "id": "proposals-estimates",
      "label": "Proposals & estimates",
      "description": "Hub proposals, drafts, send to customer, estimate library.",
      "risk": "high",
      "toolCount": 31
    },
    {
      "id": "jobs-files",
      "label": "Jobs & files",
      "description": "Create/edit jobs, uploads, work orders, delete files, rename, hours, assign tasks — full job file ops (use Job status & limbo scope for status-only keys). Does not include Home File gate codes, pets, formulas, or leftovers.",
      "risk": "high",
      "toolCount": 17
    },
    {
      "id": "home-file",
      "label": "Home File",
      "description": "Home File timeline (no prices) plus staff-only gate code, lockbox, access notes and pets, exact color formulas, and the leftover paint log. This grant is separate from Jobs & files, so an existing jobs-files key does not gain gate-code access. Gate codes and pets are never included in anything customer-facing.",
      "risk": "high",
      "toolCount": 4
    },
    {
      "id": "scheduling",
      "label": "Scheduling & visits",
      "description": "Crew Schedule / admin assistant parity: read jobs/crew/shifts, edit bars & spots, assign crew, estimate/client visits, delay posts, and schedule-related job fields (hours). write_schedule_with_ai stays in-app (Pro Tips panel) — use edit_job_schedule or visit tools from automations.",
      "risk": "medium",
      "toolCount": 21
    },
    {
      "id": "job-delays",
      "label": "Job delay posts",
      "description": "Post rain/heat/schedule delays (customer share + crew cascade) with optional polish_delay_reason — same as check-in Delay / Action Feed.",
      "risk": "medium",
      "toolCount": 2
    },
    {
      "id": "job-status",
      "label": "Job status & limbo",
      "description": "Set job status (active, accepted, limbo, completed, archived), limbo staff reason, general/T&M flags — includes polish_limbo_reason (Rewrite with AI) or custom text.",
      "risk": "medium",
      "toolCount": 4
    },
    {
      "id": "crew-clock",
      "label": "Crew & time clock",
      "description": "Clock in/out, shifts, crew roster edits.",
      "risk": "high",
      "toolCount": 7
    },
    {
      "id": "production-rates",
      "label": "Production rates",
      "description": "Rate book list/get/upsert/delete.",
      "risk": null,
      "toolCount": 4
    },
    {
      "id": "drips-comms",
      "label": "Email & drips",
      "description": "Templates and drip sequence drafts/updates.",
      "risk": null,
      "toolCount": 7
    },
    {
      "id": "business-data",
      "label": "Business data (read)",
      "description": "Portal Ops data API (read-only, paged, filter by date / customer / job): leads, contacts, estimates, schedule, production, photos, messages, follow-ups, staff, hours, colors, reviews/referrals, settings, activity. Invoices / payments / job costing stay on owner-finance.",
      "risk": null,
      "toolCount": 18
    },
    {
      "id": "photos",
      "label": "Photos (upload & annotate)",
      "description": "upload_photo / save_annotated_photo (new linked copy, original untouched) + list_photos. Every assistant photo is pending until staff approve it in Portal; approval is not available to keys.",
      "risk": "medium",
      "toolCount": 6
    },
    {
      "id": "webhooks",
      "label": "Event webhooks",
      "description": "list_webhooks / create_webhook / delete_webhook — HMAC-signed POSTs for lead.created, customer.replied, proposal.viewed, proposal.signed, payment.received.",
      "risk": "medium",
      "toolCount": 3
    },
    {
      "id": "owner-finance",
      "label": "Owner $ — Numbers & job costing",
      "description": "Margin, burden, CFO packs, bank inbox/reconciliation, invoices, payments, job costing, save_numbers_settings, save_job_costing, record_collected_payment, receipts → job costing (ingest/confirm/list/void).",
      "risk": "high",
      "toolCount": 22
    },
    {
      "id": "app-import",
      "label": "Import from another app",
      "description": "PaintScout export / spreadsheet import: preview_app_import (stages a preview, duplicates flagged), commit_app_import (idempotent per previewId), list_app_import_batches, undo_app_import. Writes contacts, jobs, estimates, invoices with customerNotifySuppressed — no email, SMS, drips, proposal sends, or reminders.",
      "risk": "high",
      "toolCount": 4
    }
  ],
  "x-portal-ops-scope-appendix": [
    {
      "id": "briefing-read",
      "label": "Morning brief & reads",
      "description": "Jobs, crew, clock scans, learning AI, recurring reminders, check-ins. Home File, including gate codes, is a separate Home File grant.",
      "risk": null,
      "tools": [
        "analyze_time_trends",
        "list_crew",
        "list_jobs",
        "list_learning_ai",
        "list_recurring_reminders",
        "list_teams",
        "list_tools",
        "read_job",
        "scan_shifts"
      ]
    },
    {
      "id": "action-feed",
      "label": "Tasks & alerts",
      "description": "Full Action Feed cleanup for this org: list/approve pending check-ins, one-off reminders — same mutate surface as Today (not read-only).",
      "risk": "medium",
      "tools": [
        "add_reminder",
        "list_pending_checkins",
        "publish_checkins"
      ]
    },
    {
      "id": "company-state",
      "label": "Company memory & inventory",
      "description": "Company profile, lore (remember/forget), tools checkout, reminders.",
      "risk": "medium",
      "tools": [
        "add_reminder",
        "add_tool",
        "check_in_tool",
        "check_out_tool",
        "create_recurring_reminder",
        "delete_recurring_reminder",
        "delete_tool",
        "edit_recurring_reminder",
        "edit_tool",
        "forget",
        "mark_tool_fixed",
        "mark_tool_repair",
        "remember",
        "toggle_recurring_reminder",
        "update_company_info"
      ]
    },
    {
      "id": "leads-crm",
      "label": "Leads & pipeline",
      "description": "list_leads and get_lead return full website CRM records and linked consent docs (including phone numbers). Pipeline scoring, follow-up drafts, and create_lead. list_sales_leads is Sales hub to-build jobs, not these CRM records.",
      "risk": null,
      "tools": [
        "analyze_pipeline",
        "create_lead",
        "draft_followup",
        "get_lead",
        "list_leads",
        "list_sales_leads",
        "score_lead"
      ]
    },
    {
      "id": "proposals-estimates",
      "label": "Proposals & estimates",
      "description": "Hub proposals, drafts, send to customer, estimate library.",
      "risk": "high",
      "tools": [
        "add_estimate_note",
        "analyze_rate_book",
        "create_estimate",
        "delete_estimate_descriptor",
        "delete_estimate_product",
        "download_proposal_pdf",
        "estimate_context",
        "estimate_snapshot",
        "get_estimate_descriptor",
        "get_estimate_product",
        "link_recording",
        "list_estimate_descriptors",
        "list_estimate_products",
        "list_proposals",
        "list_recordings",
        "read_estimate_handoff",
        "read_proposal",
        "read_recording_transcript",
        "reopen_estimate_draft",
        "reopen_proposal_draft",
        "research_estimate_product",
        "resend_proposal_email",
        "restore_proposal_revision",
        "save_estimate_settings",
        "save_proposal_draft",
        "send_proposal",
        "unlink_recording",
        "unsend_proposal_to_draft",
        "update_estimate",
        "upsert_estimate_descriptor",
        "upsert_estimate_product"
      ]
    },
    {
      "id": "jobs-files",
      "label": "Jobs & files",
      "description": "Create/edit jobs, uploads, work orders, delete files, rename, hours, assign tasks — full job file ops (use Job status & limbo scope for status-only keys). Does not include Home File gate codes, pets, formulas, or leftovers.",
      "risk": "high",
      "tools": [
        "add_job",
        "add_task_to_job",
        "assign_to_job",
        "attach_job_file",
        "attach_work_order",
        "create_job",
        "delete_job_file",
        "detach_job_file",
        "edit_job",
        "edit_summary",
        "link_recording",
        "remove_job_file",
        "rename_job",
        "set_job_colors",
        "set_job_name",
        "unlink_recording",
        "upload_job_file"
      ]
    },
    {
      "id": "home-file",
      "label": "Home File",
      "description": "Home File timeline (no prices) plus staff-only gate code, lockbox, access notes and pets, exact color formulas, and the leftover paint log. This grant is separate from Jobs & files, so an existing jobs-files key does not gain gate-code access. Gate codes and pets are never included in anything customer-facing.",
      "risk": "high",
      "tools": [
        "read_home_file",
        "set_home_file_access",
        "set_home_file_formulas",
        "set_home_file_leftovers"
      ]
    },
    {
      "id": "scheduling",
      "label": "Scheduling & visits",
      "description": "Crew Schedule / admin assistant parity: read jobs/crew/shifts, edit bars & spots, assign crew, estimate/client visits, delay posts, and schedule-related job fields (hours). write_schedule_with_ai stays in-app (Pro Tips panel) — use edit_job_schedule or visit tools from automations.",
      "risk": "medium",
      "tools": [
        "add_spot_assignment",
        "analyze_time_trends",
        "assign_to_job",
        "book_client_visit",
        "book_estimate_visit",
        "cancel_client_visit",
        "cancel_estimate_visit",
        "edit_job",
        "edit_job_schedule",
        "list_appointments",
        "list_crew",
        "list_jobs",
        "list_recurring_reminders",
        "list_schedule",
        "list_teams",
        "offer_estimate_visit",
        "polish_delay_reason",
        "post_job_delay",
        "read_job",
        "remove_spot_assignment",
        "scan_shifts"
      ]
    },
    {
      "id": "job-delays",
      "label": "Job delay posts",
      "description": "Post rain/heat/schedule delays (customer share + crew cascade) with optional polish_delay_reason — same as check-in Delay / Action Feed.",
      "risk": "medium",
      "tools": [
        "polish_delay_reason",
        "post_job_delay"
      ]
    },
    {
      "id": "job-status",
      "label": "Job status & limbo",
      "description": "Set job status (active, accepted, limbo, completed, archived), limbo staff reason, general/T&M flags — includes polish_limbo_reason (Rewrite with AI) or custom text.",
      "risk": "medium",
      "tools": [
        "list_jobs",
        "polish_limbo_reason",
        "read_job",
        "set_job_status"
      ]
    },
    {
      "id": "crew-clock",
      "label": "Crew & time clock",
      "description": "Clock in/out, shifts, crew roster edits.",
      "risk": "high",
      "tools": [
        "add_crew_member",
        "add_shift_manual",
        "clock_in_user",
        "clock_out_user",
        "delete_shift",
        "edit_crew_member",
        "edit_shift"
      ]
    },
    {
      "id": "production-rates",
      "label": "Production rates",
      "description": "Rate book list/get/upsert/delete.",
      "risk": null,
      "tools": [
        "delete_production_rate",
        "get_production_rate",
        "list_production_rates",
        "upsert_production_rate"
      ]
    },
    {
      "id": "drips-comms",
      "label": "Email & drips",
      "description": "Templates and drip sequence drafts/updates.",
      "risk": null,
      "tools": [
        "create_drip_sequence",
        "draft_drip_sequence",
        "draft_email_template",
        "list_drip_sequences",
        "list_email_templates",
        "update_drip_sequence",
        "update_email_template"
      ]
    },
    {
      "id": "business-data",
      "label": "Business data (read)",
      "description": "Portal Ops data API (read-only, paged, filter by date / customer / job): leads, contacts, estimates, schedule, production, photos, messages, follow-ups, staff, hours, colors, reviews/referrals, settings, activity. Invoices / payments / job costing stay on owner-finance.",
      "risk": null,
      "tools": [
        "get_contact",
        "get_estimate",
        "get_job_production",
        "get_lead",
        "get_settings",
        "list_activity",
        "list_contacts",
        "list_estimates",
        "list_followups",
        "list_hours",
        "list_job_colors",
        "list_leads",
        "list_messages",
        "list_photos",
        "list_production",
        "list_reviews_referrals",
        "list_schedule",
        "list_staff"
      ]
    },
    {
      "id": "photos",
      "label": "Photos (upload & annotate)",
      "description": "upload_photo / save_annotated_photo (new linked copy, original untouched) + list_photos. Every assistant photo is pending until staff approve it in Portal; approval is not available to keys.",
      "risk": "medium",
      "tools": [
        "annotate_photo",
        "download_photo",
        "list_pending_share_photos",
        "list_photos",
        "save_annotated_photo",
        "upload_photo"
      ]
    },
    {
      "id": "webhooks",
      "label": "Event webhooks",
      "description": "list_webhooks / create_webhook / delete_webhook — HMAC-signed POSTs for lead.created, customer.replied, proposal.viewed, proposal.signed, payment.received.",
      "risk": "medium",
      "tools": [
        "create_webhook",
        "delete_webhook",
        "list_webhooks"
      ]
    },
    {
      "id": "owner-finance",
      "label": "Owner $ — Numbers & job costing",
      "description": "Margin, burden, CFO packs, bank inbox/reconciliation, invoices, payments, job costing, save_numbers_settings, save_job_costing, record_collected_payment, receipts → job costing (ingest/confirm/list/void).",
      "risk": "high",
      "tools": [
        "advise_cfo",
        "analyze_numbers",
        "cfo_setup_status",
        "confirm_receipt",
        "draft_cfo_report",
        "finish_bank_reconciliation",
        "get_invoice",
        "ingest_receipt",
        "list_bank_inbox",
        "list_cfo_packs",
        "list_close_checklist",
        "list_invoices",
        "list_job_costs",
        "list_payments",
        "list_receipts",
        "match_bank_line",
        "query_cfo_knowledge",
        "record_collected_payment",
        "save_job_costing",
        "save_numbers_settings",
        "suggest_burden_from_wages",
        "void_receipt"
      ]
    },
    {
      "id": "app-import",
      "label": "Import from another app",
      "description": "PaintScout export / spreadsheet import: preview_app_import (stages a preview, duplicates flagged), commit_app_import (idempotent per previewId), list_app_import_batches, undo_app_import. Writes contacts, jobs, estimates, invoices with customerNotifySuppressed — no email, SMS, drips, proposal sends, or reminders.",
      "risk": "high",
      "tools": [
        "commit_app_import",
        "list_app_import_batches",
        "preview_app_import",
        "undo_app_import"
      ]
    }
  ],
  "x-portal-ops-tools": [
    {
      "name": "add_crew_member",
      "kind": "write",
      "description": "Create a crew user account. The admin will set the password themselves — this tool just creates the record.",
      "x-ops-scopes": [
        "crew-clock"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_add_crew_member"
      },
      "x-examples": [
        {
          "summary": "add_crew_member — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "add_crew_member",
            "input": {
              "name": "example",
              "username": "example",
              "role": "crew"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "add_crew_member — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "add_crew_member",
          "input": {
            "name": "example",
            "username": "example",
            "role": "crew"
          }
        }
      }
    },
    {
      "name": "add_estimate_note",
      "kind": "write",
      "description": "After working on an estimate, append a short INTERNAL handoff so the next agent can resume: what changed, decisions/assumptions, unresolved questions and next steps. Check read_estimate_handoff first. Visible to estimator in Office handoff; never on the client proposal, PDF, share page, or customer AI. Atomic append preserves existing staff notes. Max 1,000 characters per note; 4,000 total. Requires proposals-estimates. No orgId.",
      "x-ops-scopes": [
        "proposals-estimates"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_add_estimate_note"
      },
      "x-examples": [
        {
          "summary": "Leave internal context for the next agent",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "add_estimate_note",
            "input": {
              "jobId": "job-123",
              "note": "Updated bedroom walls and ceiling. Confirm trim sheen before sending.",
              "idempotencyKey": "bedroom-handoff-1"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "Leave internal context for the next agent",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "add_estimate_note",
          "input": {
            "jobId": "job-123",
            "note": "Updated bedroom walls and ceiling. Confirm trim sheen before sending.",
            "idempotencyKey": "bedroom-handoff-1"
          }
        }
      }
    },
    {
      "name": "add_job",
      "kind": "write",
      "description": "Create a new job. Job name/number MUST be the PO/quote number (e.g. '1253'), NOT the customer name — put the person in customerName. Street goes in address. Optional customerPhone / customerEmail / amount / status (active|accepted). Optional pdfBase64 + fileName attaches a work-order PDF in the same call. Set generalJob=true for Shop/overhead. Set billingType='time_and_materials' for T&M.",
      "x-ops-scopes": [
        "jobs-files"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_add_job"
      },
      "x-examples": [
        {
          "summary": "add_job — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "add_job",
            "input": {
              "name": "1253",
              "jobNumber": "example",
              "poNumber": "example",
              "quoteNumber": "example",
              "customerName": "example",
              "address": "example",
              "phone": "5735550100",
              "customerPhone": "5735550100",
              "email": "example",
              "customerEmail": "example",
              "amount": 1,
              "soldUsd": 1,
              "status": "active",
              "hoursEstimate": 1,
              "notes": "example",
              "generalJob": false,
              "billingType": "fixed",
              "pdfBase64": "example",
              "fileName": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "add_job — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "add_job",
          "input": {
            "name": "1253",
            "jobNumber": "example",
            "poNumber": "example",
            "quoteNumber": "example",
            "customerName": "example",
            "address": "example",
            "phone": "5735550100",
            "customerPhone": "5735550100",
            "email": "example",
            "customerEmail": "example",
            "amount": 1,
            "soldUsd": 1,
            "status": "active",
            "hoursEstimate": 1,
            "notes": "example",
            "generalJob": false,
            "billingType": "fixed",
            "pdfBase64": "example",
            "fileName": "example"
          }
        }
      }
    },
    {
      "name": "add_reminder",
      "kind": "write",
      "description": "Send a one-off push-notification reminder to one or more crew members RIGHT NOW. Use when the admin says 'remind Ben to grab the sprayer', 'nudge Malachi about the Johnson walkthrough tomorrow', 'tell Steve to clock out', 'send everyone a heads up about the team meeting at 3'. The reminder appears instantly on every logged-in device the recipient has. For RECURRING reminders ('every Monday', 'daily at 8am') use create_recurring_reminder instead. The reminder is also logged to the Reminders admin page for audit.",
      "x-ops-scopes": [
        "action-feed",
        "company-state"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_add_reminder"
      },
      "x-examples": [
        {
          "summary": "add_reminder — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "add_reminder",
            "input": {
              "recipients": [],
              "title": "example",
              "body": "example",
              "reason": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "add_reminder — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "add_reminder",
          "input": {
            "recipients": [],
            "title": "example",
            "body": "example",
            "reason": "example"
          }
        }
      }
    },
    {
      "name": "add_shift_manual",
      "kind": "write",
      "description": "Create a new shift manually for a crew member who missed clocking in or out. Use when admin says 'add Ben's shift yesterday from 8 to 4 on Johnson', 'Zach worked 3 hours on the Morgan job Tuesday morning but forgot to clock in'. The shift will be flagged as manually added. Leave clockOutTs null/omitted if the user is still on the clock. Times are JS milliseconds epoch — for 'yesterday 8am' convert relative to the current local time. Reason is required.",
      "x-ops-scopes": [
        "crew-clock"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_add_shift_manual"
      },
      "x-examples": [
        {
          "summary": "add_shift_manual — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "add_shift_manual",
            "input": {
              "userName": "example",
              "jobName": "example",
              "jobId": "JOB_ID",
              "clockInTs": 1,
              "clockOutTs": 1,
              "reason": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "add_shift_manual — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "add_shift_manual",
          "input": {
            "userName": "example",
            "jobName": "example",
            "jobId": "JOB_ID",
            "clockInTs": 1,
            "clockOutTs": 1,
            "reason": "example"
          }
        }
      }
    },
    {
      "name": "add_spot_assignment",
      "kind": "write",
      "description": "Put ONE crew member on ONE job for a specific day or short range WITHOUT changing that job's scheduled date range or its regular assignees. Use when the admin says 'send Seth to 1212 Main tomorrow just for the day', 'I need Ben on the Johnson job Thursday and Friday', 'put someone on the delayed Elm job for one day to finish trim'. This is the flexible spot/day-level staffing overlay — the schedule bar stays exactly where it is. For permanently assigning someone to a whole job use assign_to_job; for moving the whole job's dates use edit_job_schedule. Resolve userName via list_crew and jobName/jobId via list_jobs if unsure.",
      "x-ops-scopes": [
        "scheduling"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_add_spot_assignment"
      },
      "x-examples": [
        {
          "summary": "add_spot_assignment — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "add_spot_assignment",
            "input": {
              "jobId": "JOB_ID",
              "jobName": "example",
              "userName": "example",
              "start": "example",
              "end": "example",
              "note": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "add_spot_assignment — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "add_spot_assignment",
          "input": {
            "jobId": "JOB_ID",
            "jobName": "example",
            "userName": "example",
            "start": "example",
            "end": "example",
            "note": "example"
          }
        }
      }
    },
    {
      "name": "add_task_to_job",
      "kind": "write",
      "description": "Add a single checklist/task line to a job's summary (e.g. 'Prime hallway').",
      "x-ops-scopes": [
        "jobs-files"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_add_task_to_job"
      },
      "x-examples": [
        {
          "summary": "add_task_to_job — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "add_task_to_job",
            "input": {
              "jobId": "JOB_ID",
              "task": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "add_task_to_job — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "add_task_to_job",
          "input": {
            "jobId": "JOB_ID",
            "task": "example"
          }
        }
      }
    },
    {
      "name": "add_tool",
      "kind": "write",
      "description": "Add a new tool to the shop inventory. Use when admin says 'add the titan 440 to tools', 'new tool: festool sander', etc.",
      "x-ops-scopes": [
        "company-state"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_add_tool"
      },
      "x-examples": [
        {
          "summary": "add_tool — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "add_tool",
            "input": {
              "name": "example",
              "category": "example",
              "notes": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "add_tool — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "add_tool",
          "input": {
            "name": "example",
            "category": "example",
            "notes": "example"
          }
        }
      }
    },
    {
      "name": "advise_cfo",
      "kind": "read",
      "description": "Admin-only. Return ranked CFO advisor actions grounded in existing Numbers data. Use when admin asks what to fix, what to do next financially, how to improve margin, or what money issues need attention. Cite returned fields/basis; no orgId is accepted.",
      "x-ops-scopes": [
        "owner-finance"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_advise_cfo"
      }
    },
    {
      "name": "analyze_numbers",
      "kind": "read",
      "description": "Return a compact admin-only Numbers dashboard rollup: org burden/breakeven settings (and what's missing), sales KPIs, production-month pulse (produced $, calendar labor $, completed-job cohort labor $, material budget, GP), labor efficiency overruns, hour-budget KPIs (crew mix % and who usually finishes under hours), job-type scorecard, pipeline stage counts, and CFO tips. Use when admin asks about margin, GP, close rate, breakeven, labor overrun, who is under hour budget, job-type profitability, or 'how are we doing on numbers'.",
      "x-ops-scopes": [
        "owner-finance"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_analyze_numbers"
      }
    },
    {
      "name": "analyze_pipeline",
      "kind": "read",
      "description": "Return pipeline stage counts, stuck deals (sales stages idle too long), and suggested next actions per job. Use when admin asks about pipeline health, stuck proposals, deals waiting to schedule, follow-ups, or 'what should we chase'.",
      "x-ops-scopes": [
        "leads-crm"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_analyze_pipeline"
      }
    },
    {
      "name": "analyze_rate_book",
      "kind": "read",
      "description": "Admin-only read / insight. Summarize the production-rate + descriptor libraries: counts by group/type, missing clientLabels, empty rates, thin descriptors, and billable hourlyRate. Use when admin asks to clean up production rates, review the rate book, or explain gaps. Clarifies production rates vs billable hourly vs Numbers burden.",
      "x-ops-scopes": [
        "proposals-estimates"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_analyze_rate_book"
      },
      "x-examples": [
        {
          "summary": "analyze_rate_book — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "analyze_rate_book",
            "input": {}
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "analyze_rate_book — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "analyze_rate_book",
          "input": {}
        }
      }
    },
    {
      "name": "analyze_time_trends",
      "kind": "read",
      "description": "Return a week-by-week time-clock trend report across the loaded shift history. Includes hours by week, job, and person; comparison against the previous same-length window; open-shift/manual-edit flags; and general/shop job share. Use this when admin asks about trends over multiple weeks, whether shop hours increased, what needs fixed/changed, payroll patterns, overhead creeping up, or asks you to analyze the business.",
      "x-ops-scopes": [
        "briefing-read",
        "scheduling"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_analyze_time_trends"
      }
    },
    {
      "name": "annotate_photo",
      "kind": "write",
      "description": "Alias of save_annotated_photo. Save an AI-annotated version of a photo as a NEW linked photo (parentPhotoId / parentStoragePath). The original is never modified. The copy is staff-only until approved in Portal. No orgId.",
      "x-ops-scopes": [
        "photos"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_annotate_photo"
      },
      "x-examples": [
        {
          "summary": "annotate_photo — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "annotate_photo",
            "input": {
              "sourcePhotoId": "example",
              "jobId": "JOB_ID",
              "sourceStoragePath": "example",
              "sourceUrl": "example",
              "imageBase64": "example",
              "contentType": "example",
              "caption": "example",
              "room": "example",
              "annotationNotes": "example",
              "fileName": "example",
              "photoId": "example",
              "storagePath": "example",
              "annotatedImageBase64": "example",
              "notes": "example",
              "scope": "job"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "annotate_photo — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "annotate_photo",
          "input": {
            "sourcePhotoId": "example",
            "jobId": "JOB_ID",
            "sourceStoragePath": "example",
            "sourceUrl": "example",
            "imageBase64": "example",
            "contentType": "example",
            "caption": "example",
            "room": "example",
            "annotationNotes": "example",
            "fileName": "example",
            "photoId": "example",
            "storagePath": "example",
            "annotatedImageBase64": "example",
            "notes": "example",
            "scope": "job"
          }
        }
      }
    },
    {
      "name": "assign_to_job",
      "kind": "write",
      "description": "Assign (or unassign) users and/or teams to a job. Anyone assigned will see the job auto-pinned in their sidebar. Use this when the admin says 'put Ben on the Johnson job', 'assign Malachi and Ben to 1212 Main', 'move the Panthers team to the Elm St job', 'remove Ben from the Johnson job', 'clear assignments for X'. For user assignments use their exact display name; for teams use the team id prefixed with team: (e.g. 'team:abc123'). Call list_crew and list_teams first if you don't have ids/names. Mode 'replace' overwrites the assignees list, 'add' appends, 'remove' drops the listed entries.",
      "x-ops-scopes": [
        "jobs-files",
        "scheduling"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_assign_to_job"
      },
      "x-examples": [
        {
          "summary": "assign_to_job — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "assign_to_job",
            "input": {
              "jobId": "JOB_ID",
              "mode": "replace",
              "users": [],
              "teams": []
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "assign_to_job — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "assign_to_job",
          "input": {
            "jobId": "JOB_ID",
            "mode": "replace",
            "users": [],
            "teams": []
          }
        }
      }
    },
    {
      "name": "attach_job_file",
      "kind": "write",
      "description": "Alias of upload_job_file. Attach a PaintScout / work-order PDF to an existing job. Pass jobId + pdfBase64 (or fileBase64) + fileName. Stores under orgs/{orgId}/jobs/{jobId}/… and appends job.files[]. Alias: attach_work_order.",
      "x-ops-scopes": [
        "jobs-files"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_attach_job_file"
      },
      "x-examples": [
        {
          "summary": "attach_job_file — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "attach_job_file",
            "input": {
              "jobId": "JOB_ID",
              "pdfBase64": "example",
              "fileBase64": "example",
              "fileName": "example",
              "kind": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "attach_job_file — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "attach_job_file",
          "input": {
            "jobId": "JOB_ID",
            "pdfBase64": "example",
            "fileBase64": "example",
            "fileName": "example",
            "kind": "example"
          }
        }
      }
    },
    {
      "name": "attach_work_order",
      "kind": "write",
      "description": "Alias of upload_job_file. Attach a PaintScout / work-order PDF to an existing job. Pass jobId + pdfBase64 (or fileBase64) + fileName. Stores under orgs/{orgId}/jobs/{jobId}/… and appends job.files[]. Alias: attach_work_order.",
      "x-ops-scopes": [
        "jobs-files"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_attach_work_order"
      },
      "x-examples": [
        {
          "summary": "attach_work_order — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "attach_work_order",
            "input": {
              "jobId": "JOB_ID",
              "pdfBase64": "example",
              "fileBase64": "example",
              "fileName": "example",
              "kind": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "attach_work_order — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "attach_work_order",
          "input": {
            "jobId": "JOB_ID",
            "pdfBase64": "example",
            "fileBase64": "example",
            "fileName": "example",
            "kind": "example"
          }
        }
      }
    },
    {
      "name": "book_client_visit",
      "kind": "write",
      "description": "Book a non-estimate-slot visit on job.clientVisits[] (existing client, site visit, color consult, etc.). visitKind: estimate|site_visit|color_consult|punch_list|other (see list_appointments.visitKinds).",
      "x-ops-scopes": [
        "scheduling"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_book_client_visit"
      },
      "x-examples": [
        {
          "summary": "book_client_visit — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "book_client_visit",
            "input": {
              "jobId": "JOB_ID",
              "jobName": "example",
              "startAt": 1,
              "endAt": 1,
              "visitKind": "example",
              "visitId": "example",
              "estimatorUserId": "example",
              "estimatorName": "example",
              "notes": "example",
              "customerConfirmed": false,
              "customerConfirmationNote": "example",
              "notifyCustomer": false
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "book_client_visit — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "book_client_visit",
          "input": {
            "jobId": "JOB_ID",
            "jobName": "example",
            "startAt": 1,
            "endAt": 1,
            "visitKind": "example",
            "visitId": "example",
            "estimatorUserId": "example",
            "estimatorName": "example",
            "notes": "example",
            "customerConfirmed": false,
            "customerConfirmationNote": "example",
            "notifyCustomer": false
          }
        }
      }
    },
    {
      "name": "book_estimate_visit",
      "kind": "write",
      "description": "Book or move an estimate visit on job.estimateAppointment (portal + estimator Google Calendar when connected). Use for 'schedule Kim for Tuesday 2pm', 'book the 1253 estimate visit'; booking again moves it. Times as startAt/endAt epoch ms or ISO. Pass estimatorUserId or estimatorName. Never emails or texts the customer. Portal Ops keys: only after the customer confirmed the time (customerConfirmed: true); automatic customer reminders stay off unless notifyCustomer: true.",
      "x-ops-scopes": [
        "scheduling"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_book_estimate_visit"
      },
      "x-examples": [
        {
          "summary": "book_estimate_visit — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "book_estimate_visit",
            "input": {
              "jobId": "JOB_ID",
              "jobName": "example",
              "startAt": 1,
              "endAt": 1,
              "estimatorUserId": "example",
              "estimatorName": "example",
              "syncGoogle": false,
              "notes": "example",
              "customerConfirmed": false,
              "customerConfirmationNote": "example",
              "notifyCustomer": false
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "book_estimate_visit — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "book_estimate_visit",
          "input": {
            "jobId": "JOB_ID",
            "jobName": "example",
            "startAt": 1,
            "endAt": 1,
            "estimatorUserId": "example",
            "estimatorName": "example",
            "syncGoogle": false,
            "notes": "example",
            "customerConfirmed": false,
            "customerConfirmationNote": "example",
            "notifyCustomer": false
          }
        }
      }
    },
    {
      "name": "cancel_client_visit",
      "kind": "write",
      "description": "Remove one row from job.clientVisits by visitId.",
      "x-ops-scopes": [
        "scheduling"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_cancel_client_visit"
      },
      "x-examples": [
        {
          "summary": "cancel_client_visit — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "cancel_client_visit",
            "input": {
              "jobId": "JOB_ID",
              "jobName": "example",
              "visitId": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "cancel_client_visit — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "cancel_client_visit",
          "input": {
            "jobId": "JOB_ID",
            "jobName": "example",
            "visitId": "example"
          }
        }
      }
    },
    {
      "name": "cancel_estimate_visit",
      "kind": "write",
      "description": "Clear job.estimateAppointment (does not delete Google event or email customer cancel — follow up in Portal if needed).",
      "x-ops-scopes": [
        "scheduling"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_cancel_estimate_visit"
      },
      "x-examples": [
        {
          "summary": "cancel_estimate_visit — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "cancel_estimate_visit",
            "input": {
              "jobId": "JOB_ID",
              "jobName": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "cancel_estimate_visit — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "cancel_estimate_visit",
          "input": {
            "jobId": "JOB_ID",
            "jobName": "example"
          }
        }
      }
    },
    {
      "name": "cfo_setup_status",
      "kind": "read",
      "description": "Admin-only. Return what Numbers/CFO settings are missing, a short how-to, crew name hints, and the next question to ask. Call this FIRST when admin asks for help setting up Numbers, CFO, burden, overhead, or job costing rates — whether brand-new or mid-setup. Never invent wages. No orgId.",
      "x-ops-scopes": [
        "owner-finance"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_cfo_setup_status"
      }
    },
    {
      "name": "check_in_tool",
      "kind": "write",
      "description": "Mark a tool as returned to the shop and available. Use when someone says 'I brought the X back' or 'X is back at shop'.",
      "x-ops-scopes": [
        "company-state"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_check_in_tool"
      },
      "x-examples": [
        {
          "summary": "check_in_tool — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "check_in_tool",
            "input": {
              "toolId": "example",
              "byName": "example",
              "note": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "check_in_tool — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "check_in_tool",
          "input": {
            "toolId": "example",
            "byName": "example",
            "note": "example"
          }
        }
      }
    },
    {
      "name": "check_out_tool",
      "kind": "write",
      "description": "Mark a tool as at a specific job. Use when a crew member says 'I grabbed the X' or 'taking the Y to 1297' or admin says 'assign X to 1297'. The app auto-sets who and when.",
      "x-ops-scopes": [
        "company-state"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_check_out_tool"
      },
      "x-examples": [
        {
          "summary": "check_out_tool — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "check_out_tool",
            "input": {
              "toolId": "example",
              "jobId": "JOB_ID",
              "byName": "example",
              "note": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "check_out_tool — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "check_out_tool",
          "input": {
            "toolId": "example",
            "jobId": "JOB_ID",
            "byName": "example",
            "note": "example"
          }
        }
      }
    },
    {
      "name": "clock_in_user",
      "kind": "write",
      "description": "Clock a crew member in on a job, right now, on behalf of the admin. Use when admin says 'clock Ben in on the Johnson house', 'punch Steve into Beadling', 'start Zach's timer on the Morgan repaint'. GPS is skipped (admin isn't physically at the job), and the shift is flagged with admin's name as the on-behalf authorizer. If the user is already clocked in, the call fails — tell the admin to clock them out first. Resolve userName via list_crew and jobName via list_jobs if you're not sure.",
      "x-ops-scopes": [
        "crew-clock"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_clock_in_user"
      },
      "x-examples": [
        {
          "summary": "clock_in_user — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "clock_in_user",
            "input": {
              "userName": "example",
              "jobName": "example",
              "jobId": "JOB_ID"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "clock_in_user — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "clock_in_user",
          "input": {
            "userName": "example",
            "jobName": "example",
            "jobId": "JOB_ID"
          }
        }
      }
    },
    {
      "name": "clock_out_user",
      "kind": "write",
      "description": "Clock a crew member OUT of their current open shift, right now, on behalf of the admin. Use when admin says 'clock Ben out', 'end Steve's timer', 'Zach forgot to clock out'. Fails silently if the user isn't currently on the clock. Check the 'Currently on the clock' section of the TIME CLOCK block to see who's punched in.",
      "x-ops-scopes": [
        "crew-clock"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_clock_out_user"
      },
      "x-examples": [
        {
          "summary": "clock_out_user — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "clock_out_user",
            "input": {
              "userName": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "clock_out_user — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "clock_out_user",
          "input": {
            "userName": "example"
          }
        }
      }
    },
    {
      "name": "commit_app_import",
      "kind": "write",
      "description": "Admin-only MUTATING. Write one staged preview from preview_app_import. Idempotent per previewId: calling it again returns the same importBatchId with alreadyCommitted true and writes nothing new; a commit that stopped partway resumes the same batch. Every record carries customerNotifySuppressed — no email, SMS, drips, proposal sends, or reminders. NEVER pass orgId.",
      "x-ops-scopes": [
        "app-import"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_commit_app_import"
      },
      "x-examples": [
        {
          "summary": "commit_app_import — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "commit_app_import",
            "input": {
              "previewId": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "commit_app_import — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "commit_app_import",
          "input": {
            "previewId": "example"
          }
        }
      }
    },
    {
      "name": "confirm_receipt",
      "kind": "write",
      "description": "Portal Ops only (Owner $ scope). MUTATING. Post an ingested receipt to job costing: ONE expense line (vendor, invoice number, receipt id) and the file attached to that job's costing. REFUSES with needs-review unless the match confidence is at least 0.85 AND exactly one job fits — those wait for a person in Numbers. A receipt with no job at all posts to overhead. jobId is optional and can only agree with the matched job; it never overrides the match. Safe to repeat: a second confirm returns alreadyConfirmed and adds nothing. NEVER QuickBooks. NEVER pass orgId.",
      "x-ops-scopes": [
        "owner-finance"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_confirm_receipt"
      },
      "x-examples": [
        {
          "summary": "confirm_receipt — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "confirm_receipt",
            "input": {
              "id": "example",
              "jobId": "JOB_ID"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "confirm_receipt — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "confirm_receipt",
          "input": {
            "id": "example",
            "jobId": "JOB_ID"
          }
        }
      }
    },
    {
      "name": "create_drip_sequence",
      "kind": "write",
      "description": "Admin-only. Create a custom drip automation in the active org. Confirm required. Prefer draft_drip_sequence first. No orgId.",
      "x-ops-scopes": [
        "drips-comms"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_create_drip_sequence"
      },
      "x-examples": [
        {
          "summary": "create_drip_sequence — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "create_drip_sequence",
            "input": {
              "name": "example",
              "enabled": false,
              "trigger": {},
              "cancelOnEvents": [],
              "steps": []
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "create_drip_sequence — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "create_drip_sequence",
          "input": {
            "name": "example",
            "enabled": false,
            "trigger": {},
            "cancelOnEvents": [],
            "steps": []
          }
        }
      }
    },
    {
      "name": "create_estimate",
      "kind": "write",
      "description": "Create a blank draft Portal estimate attached to an EXISTING job (stamps job.estimateId). Required: jobId. Does NOT create a job, send/share/publish, or invent takeoff. Idempotent if the job already has a draft. After create, use save_proposal_draft to fill rooms/descriptors. No orgId.",
      "x-ops-scopes": [
        "proposals-estimates"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_create_estimate"
      },
      "x-examples": [
        {
          "summary": "Blank draft on existing job",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "create_estimate",
            "input": {
              "jobId": "lead-GHU5ZAWpEXvTB9D9T2Tr",
              "estimateType": "Interior"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "Blank draft on existing job",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "create_estimate",
          "input": {
            "jobId": "lead-GHU5ZAWpEXvTB9D9T2Tr",
            "estimateType": "Interior"
          }
        }
      }
    },
    {
      "name": "create_job",
      "kind": "write",
      "description": "Alias of add_job. Create a new job. Job name/number MUST be the PO/quote number (e.g. '1253'), NOT the customer name — put the person in customerName. Street goes in address. Optional customerPhone / customerEmail / amount / status (active|accepted). Optional pdfBase64 + fileName attaches a work-order PDF in the same call. Set generalJob=true for Shop/overhead. Set billingType='time_and_materials' for T&M.",
      "x-ops-scopes": [
        "jobs-files"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_create_job"
      },
      "x-examples": [
        {
          "summary": "create_job — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "create_job",
            "input": {
              "name": "1253",
              "jobNumber": "example",
              "poNumber": "example",
              "quoteNumber": "example",
              "customerName": "example",
              "address": "example",
              "phone": "5735550100",
              "customerPhone": "5735550100",
              "email": "example",
              "customerEmail": "example",
              "amount": 1,
              "soldUsd": 1,
              "status": "active",
              "hoursEstimate": 1,
              "notes": "example",
              "generalJob": false,
              "billingType": "fixed",
              "pdfBase64": "example",
              "fileName": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "create_job — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "create_job",
          "input": {
            "name": "1253",
            "jobNumber": "example",
            "poNumber": "example",
            "quoteNumber": "example",
            "customerName": "example",
            "address": "example",
            "phone": "5735550100",
            "customerPhone": "5735550100",
            "email": "example",
            "customerEmail": "example",
            "amount": 1,
            "soldUsd": 1,
            "status": "active",
            "hoursEstimate": 1,
            "notes": "example",
            "generalJob": false,
            "billingType": "fixed",
            "pdfBase64": "example",
            "fileName": "example"
          }
        }
      }
    },
    {
      "name": "create_lead",
      "kind": "write",
      "description": "Add a phone-call / callback lead on the Sales leads list (orgs/{orgId}/leads) and bootstrap an estimate job. Use when someone calls and you have a phone number. Requires phone (10 digits). Optional name, address, notes. Returns leadId + jobId. Then book_estimate_visit on that jobId. Does not text or email the customer. Pass toolUseId for idempotent retries. No orgId.",
      "x-ops-scopes": [
        "leads-crm"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_create_lead"
      },
      "x-examples": [
        {
          "summary": "create_lead — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "create_lead",
            "input": {
              "phone": "5735550100",
              "name": "Kim",
              "address": "example",
              "notes": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "create_lead — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "create_lead",
          "input": {
            "phone": "5735550100",
            "name": "Kim",
            "address": "example",
            "notes": "example"
          }
        }
      }
    },
    {
      "name": "create_recurring_reminder",
      "kind": "write",
      "description": "Create a RECURRING scheduled push reminder (fires automatically on its schedule until deleted/paused). Use when admin says things like 'remind the leads every Monday at 7am to do job walkthroughs', 'every weekday at 5pm tell crew to clock out', 'daily at 8am nudge Ben to check tools', 'every 3 days remind me to review payroll'. DIFFERENT from add_reminder (which fires once, right now) — this one repeats on a schedule.\n\nCONDITIONAL TARGETING: If the admin wants the reminder to only go to people matching a live condition at fire time — like 'only folks still clocked in' or 'only folks who haven't worked today' — use the `filter` param. 'still-on-clock' = only users who are currently clocked in (open shift, no clockOut yet). 'no-shift-today' = only users with no shift started today yet. Great for 5pm clock-out nudges (filter: still-on-clock) or morning missed-clock-in nudges (filter: no-shift-today). Default is 'none' = send to every resolved target regardless of their shift state.\n\nCONVERSATIONAL FLOW: If the admin gives a vague request like 'I want to set up a reminder' or 'make a weekly reminder', walk them through it ONE QUESTION AT A TIME in chat — don't call the tool until you have all the info. Ask in this order (skip a step if they already answered it): (1) what's the reminder about? / 'what should it say?'  (2) who gets it? (everyone, admins, leads, crew, or specific people) (3) how often? (daily, weekly on which days, or every N days) (4) what time of day? (5) ONLY if it sounds relevant — any conditions? (still on clock / no shift today / none). Once you have all info, call create_recurring_reminder with everything filled in. Keep each question to one short line. Do NOT list all questions at once.\n\nIf the admin gives everything in one message ('every Tuesday at 9am remind crew to check their trucks') call the tool immediately — no back-and-forth needed.",
      "x-ops-scopes": [
        "company-state"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_create_recurring_reminder"
      },
      "x-examples": [
        {
          "summary": "create_recurring_reminder — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "create_recurring_reminder",
            "input": {
              "title": "example",
              "body": "example",
              "schedule": {},
              "targets": {},
              "filter": "none",
              "enabled": false
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "create_recurring_reminder — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "create_recurring_reminder",
          "input": {
            "title": "example",
            "body": "example",
            "schedule": {},
            "targets": {},
            "filter": "none",
            "enabled": false
          }
        }
      }
    },
    {
      "name": "create_webhook",
      "kind": "write",
      "description": "Subscribe an https URL to events: lead.created, customer.replied, proposal.viewed, proposal.signed, payment.received. Returns the HMAC signing secret ONCE. Payloads carry record ids only. No orgId.",
      "x-ops-scopes": [
        "webhooks"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_create_webhook"
      },
      "x-examples": [
        {
          "summary": "create_webhook — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "create_webhook",
            "input": {
              "url": "example",
              "events": [],
              "label": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "create_webhook — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "create_webhook",
          "input": {
            "url": "example",
            "events": [],
            "label": "example"
          }
        }
      }
    },
    {
      "name": "delete_estimate_descriptor",
      "kind": "write",
      "description": "Admin-only MUTATING. Permanently delete a library descriptor. Only when admin explicitly says delete/remove. Call list_estimate_descriptors first.",
      "x-ops-scopes": [
        "proposals-estimates"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_delete_estimate_descriptor"
      },
      "x-examples": [
        {
          "summary": "delete_estimate_descriptor — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "delete_estimate_descriptor",
            "input": {
              "id": "example",
              "descriptorId": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "delete_estimate_descriptor — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "delete_estimate_descriptor",
          "input": {
            "id": "example",
            "descriptorId": "example"
          }
        }
      }
    },
    {
      "name": "delete_estimate_product",
      "kind": "write",
      "description": "Admin-only MUTATING. Permanently delete a product-library entry. Only when admin explicitly says delete/remove. Call list_estimate_products first for the id.",
      "x-ops-scopes": [
        "proposals-estimates"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_delete_estimate_product"
      },
      "x-examples": [
        {
          "summary": "delete_estimate_product — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "delete_estimate_product",
            "input": {
              "id": "example",
              "productId": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "delete_estimate_product — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "delete_estimate_product",
          "input": {
            "id": "example",
            "productId": "example"
          }
        }
      }
    },
    {
      "name": "delete_job_file",
      "kind": "write",
      "description": "Detach ONE file from an existing job's employee-facing files list (same as Photos remove). Pass jobId plus storagePath (preferred), fileName, or fileId. Exact match only — does not delete other files. Use to remove estimate/raster PDFs while keeping TEXT work orders. Aliases: remove_job_file, detach_job_file.",
      "x-ops-scopes": [
        "jobs-files"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_delete_job_file"
      },
      "x-examples": [
        {
          "summary": "delete_job_file — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "delete_job_file",
            "input": {
              "jobId": "JOB_ID",
              "storagePath": "example",
              "path": "example",
              "filePath": "example",
              "fileName": "example",
              "name": "example",
              "fileId": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "delete_job_file — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "delete_job_file",
          "input": {
            "jobId": "JOB_ID",
            "storagePath": "example",
            "path": "example",
            "filePath": "example",
            "fileName": "example",
            "name": "example",
            "fileId": "example"
          }
        }
      }
    },
    {
      "name": "delete_production_rate",
      "kind": "write",
      "description": "Admin-only MUTATING. Permanently delete a library production rate. Only when admin explicitly says delete/remove. Call list_production_rates first for the id.",
      "x-ops-scopes": [
        "production-rates"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_delete_production_rate"
      },
      "x-examples": [
        {
          "summary": "delete_production_rate — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "delete_production_rate",
            "input": {
              "id": "example",
              "rateId": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "delete_production_rate — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "delete_production_rate",
          "input": {
            "id": "example",
            "rateId": "example"
          }
        }
      }
    },
    {
      "name": "delete_recurring_reminder",
      "kind": "write",
      "description": "Permanently remove a recurring reminder. Use when admin says 'delete the Monday walkthrough reminder' or 'get rid of the clock-out reminder'. Call list_recurring_reminders first to get the id. If admin just wants to temporarily stop it, use toggle_recurring_reminder with enabled:false instead.",
      "x-ops-scopes": [
        "company-state"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_delete_recurring_reminder"
      },
      "x-examples": [
        {
          "summary": "delete_recurring_reminder — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "delete_recurring_reminder",
            "input": {
              "id": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "delete_recurring_reminder — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "delete_recurring_reminder",
          "input": {
            "id": "example"
          }
        }
      }
    },
    {
      "name": "delete_shift",
      "kind": "write",
      "description": "Permanently delete a shift from payroll. Use ONLY when admin explicitly says to delete it — 'delete that duplicate shift', 'remove the phantom clock-in on Tuesday'. NOT for corrections (use edit_shift instead). Shift IDs appear in square brackets in the TIME CLOCK block.",
      "x-ops-scopes": [
        "crew-clock"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_delete_shift"
      },
      "x-examples": [
        {
          "summary": "delete_shift — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "delete_shift",
            "input": {
              "shiftId": "example",
              "reason": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "delete_shift — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "delete_shift",
          "input": {
            "shiftId": "example",
            "reason": "example"
          }
        }
      }
    },
    {
      "name": "delete_tool",
      "kind": "write",
      "description": "Permanently remove a tool from the inventory. Only use when the admin explicitly says 'delete' or 'remove'.",
      "x-ops-scopes": [
        "company-state"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_delete_tool"
      },
      "x-examples": [
        {
          "summary": "delete_tool — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "delete_tool",
            "input": {
              "toolId": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "delete_tool — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "delete_tool",
          "input": {
            "toolId": "example"
          }
        }
      }
    },
    {
      "name": "delete_webhook",
      "kind": "write",
      "description": "Delete one webhook subscription by webhookId. No orgId.",
      "x-ops-scopes": [
        "webhooks"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_delete_webhook"
      },
      "x-examples": [
        {
          "summary": "delete_webhook — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "delete_webhook",
            "input": {
              "webhookId": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "delete_webhook — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "delete_webhook",
          "input": {
            "webhookId": "example"
          }
        }
      }
    },
    {
      "name": "detach_job_file",
      "kind": "write",
      "description": "Alias of delete_job_file. Detach ONE file from an existing job's employee-facing files list (same as Photos remove). Pass jobId plus storagePath (preferred), fileName, or fileId. Exact match only — does not delete other files. Use to remove estimate/raster PDFs while keeping TEXT work orders. Aliases: remove_job_file, detach_job_file.",
      "x-ops-scopes": [
        "jobs-files"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_detach_job_file"
      },
      "x-examples": [
        {
          "summary": "detach_job_file — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "detach_job_file",
            "input": {
              "jobId": "JOB_ID",
              "storagePath": "example",
              "path": "example",
              "filePath": "example",
              "fileName": "example",
              "name": "example",
              "fileId": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "detach_job_file — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "detach_job_file",
          "input": {
            "jobId": "JOB_ID",
            "storagePath": "example",
            "path": "example",
            "filePath": "example",
            "fileName": "example",
            "name": "example",
            "fileId": "example"
          }
        }
      }
    },
    {
      "name": "download_photo",
      "kind": "read",
      "description": "Read an existing job, estimate, check-in or assistant photo by its listed photoId. Returns a short-lived staff URL and optionally imageBase64. Org comes from auth only. Does not approve or publish it.",
      "x-ops-scopes": [
        "photos"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_download_photo"
      }
    },
    {
      "name": "download_proposal_pdf",
      "kind": "read",
      "description": "Render the customer-safe estimate PDF (same layout as staff Download estimate). Pass estimateId or jobId. Default: org-scoped signed downloadUrl (~15m) + sizeBytes; set includeBase64:true only when inline bytes are required. Cached by estimate version. No orgId.",
      "x-ops-scopes": [
        "proposals-estimates"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_download_proposal_pdf"
      },
      "x-examples": [
        {
          "summary": "download_proposal_pdf — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "download_proposal_pdf",
            "input": {
              "estimateId": "ESTIMATE_ID",
              "jobId": "JOB_ID",
              "includeBase64": false
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "download_proposal_pdf — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "download_proposal_pdf",
          "input": {
            "estimateId": "ESTIMATE_ID",
            "jobId": "JOB_ID",
            "includeBase64": false
          }
        }
      }
    },
    {
      "name": "draft_cfo_report",
      "kind": "read",
      "description": "Admin-only. Build multiple CFO packs into one report payload. Use when admin asks for a CFO report, monthly numbers report, or a job-costing packet. Default packs are month, job_costing, and advisor_actions. Cite footnotes/basis; no orgId is accepted.",
      "x-ops-scopes": [
        "owner-finance"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_draft_cfo_report"
      }
    },
    {
      "name": "draft_drip_sequence",
      "kind": "read",
      "description": "Admin-only. Preview a NEW custom drip automation (name, one or more trigger events OR stage_idle in lead/estimate_scheduled/building/sent/waiting/finished, cancel events, SMS/email steps). Does NOT create — call create_drip_sequence for Confirm. Trigger events include lead.created, proposal.sent, invoice.paid, etc. Keep placeholders exactly: {{customerName}} {{companyName}} {{shareUrl}} {{address}} {{jobName}} {{estimateNumber}} {{phone}} {{email}}. {{address}}=street; {{jobName}}/{{estimateNumber}}=estimate #. No orgId.",
      "x-ops-scopes": [
        "drips-comms"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_draft_drip_sequence"
      }
    },
    {
      "name": "draft_email_template",
      "kind": "read",
      "description": "Admin-only. Preview rewritten alert email/SMS copy for one template kind. Pass the full proposed subject/heading/body/ctaLabel/sms after the admin tells you what they want. Does NOT save — call update_email_template next for Confirm. Keep {{placeholders}} exactly. {{address}}=street only; {{jobName}}/{{estimateNumber}}=estimate # (e.g. 1319) — not the street. No orgId.",
      "x-ops-scopes": [
        "drips-comms"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_draft_email_template"
      }
    },
    {
      "name": "draft_followup",
      "kind": "read",
      "description": "Lead/admin sales tool. Draft confirm-to-send SMS and email follow-up copy for a pipeline lead/job. Never sends automatically; staff must copy or confirm elsewhere. Uses the authenticated active org only; no orgId is accepted.",
      "x-ops-scopes": [
        "leads-crm"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_draft_followup"
      }
    },
    {
      "name": "edit_crew_member",
      "kind": "write",
      "description": "Update a crew member's name, username, or role.",
      "x-ops-scopes": [
        "crew-clock"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_edit_crew_member"
      },
      "x-examples": [
        {
          "summary": "edit_crew_member — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "edit_crew_member",
            "input": {
              "userId": "example",
              "name": "example",
              "username": "example",
              "role": "crew"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "edit_crew_member — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "edit_crew_member",
          "input": {
            "userId": "example",
            "name": "example",
            "username": "example",
            "role": "crew"
          }
        }
      }
    },
    {
      "name": "edit_job",
      "kind": "write",
      "description": "Rename a job, change its address, change its status, change hour estimate, mark it as a general always-active job, or set billingType (fixed vs time_and_materials). Status values: 'estimate' (quote/lead in progress — not yet sold), 'active' (currently being worked), 'accepted' (approved but not yet started — shows on the Accepted tab), 'limbo' (still open but parked — waiting on other trades, punch list, seasonal hold, or a last visit; off the Active list; set limboReason for why), 'completed' (finished but still visible), 'archived' (lost/cancelled — hidden from active lists). Set generalJob=true for Shop/overhead buckets; they keep a normal job page but always stay active. Set billingType='time_and_materials' when the job has no fixed hours budget for progress.",
      "x-ops-scopes": [
        "jobs-files",
        "scheduling"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_edit_job"
      },
      "x-examples": [
        {
          "summary": "edit_job — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "edit_job",
            "input": {
              "jobId": "JOB_ID",
              "name": "example",
              "address": "example",
              "hoursEstimate": 1,
              "status": "estimate",
              "limboReason": "example",
              "generalJob": false,
              "billingType": "fixed",
              "soldTotal": 1
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "edit_job — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "edit_job",
          "input": {
            "jobId": "JOB_ID",
            "name": "example",
            "address": "example",
            "hoursEstimate": 1,
            "status": "estimate",
            "limboReason": "example",
            "generalJob": false,
            "billingType": "fixed",
            "soldTotal": 1
          }
        }
      }
    },
    {
      "name": "edit_job_schedule",
      "kind": "write",
      "description": "Set or change a job's SCHEDULED date range (and optionally estimated hours, crew lead, earliest-start, or scheduling notes). Use when the admin says 'schedule the Johnson job for next Monday through Wednesday', 'move 1212 Main to start the 14th', 'push the Elm St finish to Friday', 'set the crew lead on the Morgan repaint to Ben', 'block that job from starting before the 10th'. This writes the confirmed schedule the crew and customer see — it is DIFFERENT from add_spot_assignment (which drops one person on a job for a day or two without moving the bar). Dates are YYYY-MM-DD. Pass only the fields that change; anything omitted keeps its current value. IMPORTANT: when PUSHING / MOVING the start date and you are not given a new finish date, also pass endDate shifted by the same number of calendar days so the job keeps its length (the client will also auto-shift if you omit endDate). Call list_jobs first if you don't have the jobId.",
      "x-ops-scopes": [
        "scheduling"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_edit_job_schedule"
      },
      "x-examples": [
        {
          "summary": "edit_job_schedule — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "edit_job_schedule",
            "input": {
              "jobId": "JOB_ID",
              "jobName": "example",
              "startDate": "example",
              "endDate": "example",
              "estimatedHours": 1,
              "crewLeadName": "example",
              "notBefore": "example",
              "notes": "example",
              "datesLocked": false
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "edit_job_schedule — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "edit_job_schedule",
          "input": {
            "jobId": "JOB_ID",
            "jobName": "example",
            "startDate": "example",
            "endDate": "example",
            "estimatedHours": 1,
            "crewLeadName": "example",
            "notBefore": "example",
            "notes": "example",
            "datesLocked": false
          }
        }
      }
    },
    {
      "name": "edit_recurring_reminder",
      "kind": "write",
      "description": "Modify an existing recurring reminder — change its title, body, schedule, or targets. Use when admin says 'change the Monday walkthrough reminder to 6am instead', 'update the clock-out reminder body', 'add Thursday to the weekly reminder'. Call list_recurring_reminders first to get the id.\n\nPass only the fields that change — anything omitted keeps its current value. To change the schedule type entirely (e.g. from weekly to daily) pass a full new schedule object.",
      "x-ops-scopes": [
        "company-state"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_edit_recurring_reminder"
      },
      "x-examples": [
        {
          "summary": "edit_recurring_reminder — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "edit_recurring_reminder",
            "input": {
              "id": "example",
              "title": "example",
              "body": "example",
              "schedule": {},
              "targets": {},
              "filter": "none",
              "enabled": false
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "edit_recurring_reminder — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "edit_recurring_reminder",
          "input": {
            "id": "example",
            "title": "example",
            "body": "example",
            "schedule": {},
            "targets": {},
            "filter": "none",
            "enabled": false
          }
        }
      }
    },
    {
      "name": "edit_shift",
      "kind": "write",
      "description": "Modify an existing shift's clock-in time, clock-out time, or assigned job. Use when admin says 'Ben actually started at 7am not 8', 'change Steve's clock-out on the 14th to 5:30pm', 'that shift should've been on the Morgan job not Johnson'. Shift IDs appear in square brackets in the TIME CLOCK block (e.g. [abc123]). Reason is required and shows up in the audit log.",
      "x-ops-scopes": [
        "crew-clock"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_edit_shift"
      },
      "x-examples": [
        {
          "summary": "edit_shift — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "edit_shift",
            "input": {
              "shiftId": "example",
              "clockInTs": 1,
              "clockOutTs": 1,
              "jobId": "JOB_ID",
              "reason": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "edit_shift — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "edit_shift",
          "input": {
            "shiftId": "example",
            "clockInTs": 1,
            "clockOutTs": 1,
            "jobId": "JOB_ID",
            "reason": "example"
          }
        }
      }
    },
    {
      "name": "edit_summary",
      "kind": "write",
      "description": "Apply a free-form instruction to a job's summary. Use for anything narrative: 'change all walls to BM Aura Matte', 'add exclusion: radiators', 'note that lockbox code is 4421', 'fix the typo in room list'. Do NOT use this to store contract/sold/invoice dollar amounts — those go through save_job_costing so Numbers/GP can use them.",
      "x-ops-scopes": [
        "jobs-files"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_edit_summary"
      },
      "x-examples": [
        {
          "summary": "edit_summary — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "edit_summary",
            "input": {
              "jobId": "JOB_ID",
              "instruction": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "edit_summary — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "edit_summary",
          "input": {
            "jobId": "JOB_ID",
            "instruction": "example"
          }
        }
      }
    },
    {
      "name": "edit_tool",
      "kind": "write",
      "description": "Rename, re-categorize, or change notes on an existing tool. Does NOT check in/out — use check_out_tool/check_in_tool for location changes.",
      "x-ops-scopes": [
        "company-state"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_edit_tool"
      },
      "x-examples": [
        {
          "summary": "edit_tool — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "edit_tool",
            "input": {
              "toolId": "example",
              "name": "example",
              "category": "example",
              "notes": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "edit_tool — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "edit_tool",
          "input": {
            "toolId": "example",
            "name": "example",
            "category": "example",
            "notes": "example"
          }
        }
      }
    },
    {
      "name": "estimate_context",
      "kind": "read",
      "description": "First call read_estimate_handoff for the estimate being worked on. Voice session bootstrap: jobRef OR today/YYYY-MM-DD lists estimate visits that day with customer, visit window, lean estimate snapshot, linked recordings, and recordingSearch hints (visit start −15m through visit end +60m in America/Chicago + ISO, plus keywords: last name, street, PO). Visit-to-recording rule: match external recordings whose start time falls in that window; prefer title keywords. Requires briefing-read or scheduling plus proposals-estimates on the Ops key. No orgId.",
      "x-ops-scopes": [
        "proposals-estimates"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_estimate_context"
      },
      "x-examples": [
        {
          "summary": "estimate_context — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "estimate_context",
            "input": {
              "jobRef": "example",
              "date": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "estimate_context — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "estimate_context",
          "input": {
            "jobRef": "example",
            "date": "example"
          }
        }
      }
    },
    {
      "name": "estimate_snapshot",
      "kind": "read",
      "description": "First call read_estimate_handoff to check prior agent context. Lean read of an estimate draft: rooms, lines, colors, totals. Pass jobRef (PO, customer, address, or jobId). For voice sessions prefer update_estimate response totals instead of re-reading. No orgId.",
      "x-ops-scopes": [
        "proposals-estimates"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_estimate_snapshot"
      },
      "x-examples": [
        {
          "summary": "estimate_snapshot — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "estimate_snapshot",
            "input": {
              "jobRef": "example",
              "jobId": "JOB_ID"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "estimate_snapshot — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "estimate_snapshot",
          "input": {
            "jobRef": "example",
            "jobId": "JOB_ID"
          }
        }
      }
    },
    {
      "name": "finish_bank_reconciliation",
      "kind": "write",
      "description": "Admin-only MUTATING. Lock reconciliation only when difference is $0 and no uncoded lines remain. NEVER pass orgId.",
      "x-ops-scopes": [
        "owner-finance"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_finish_bank_reconciliation"
      },
      "x-examples": [
        {
          "summary": "finish_bank_reconciliation — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "finish_bank_reconciliation",
            "input": {
              "recId": "example",
              "accountId": "example",
              "openingBalanceUsd": 1,
              "statementEndBalanceUsd": 1,
              "statementEndDate": "example",
              "clearedTxnIds": []
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "finish_bank_reconciliation — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "finish_bank_reconciliation",
          "input": {
            "recId": "example",
            "accountId": "example",
            "openingBalanceUsd": 1,
            "statementEndBalanceUsd": 1,
            "statementEndDate": "example",
            "clearedTxnIds": []
          }
        }
      }
    },
    {
      "name": "forget",
      "kind": "write",
      "description": "Remove a previously-remembered fact from company lore. Use when admin says 'forget X' or 'you had it wrong, X is no longer true'.",
      "x-ops-scopes": [
        "company-state"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_forget"
      },
      "x-examples": [
        {
          "summary": "forget — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "forget",
            "input": {
              "factSubstring": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "forget — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "forget",
          "input": {
            "factSubstring": "example"
          }
        }
      }
    },
    {
      "name": "get_contact",
      "kind": "read",
      "description": "One customer contact plus history: jobs, leads, estimates, messages, and invoices (invoices only with Owner $). Read-only. No orgId.",
      "x-ops-scopes": [
        "business-data"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_get_contact"
      }
    },
    {
      "name": "get_estimate",
      "kind": "read",
      "description": "One estimate with full line items / prices plus its event trail (created, sent, viewed, signed, declined from the doc, share engagement, and sales events) and revision history. Read-only. No orgId.",
      "x-ops-scopes": [
        "business-data"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_get_estimate"
      },
      "x-examples": [
        {
          "summary": "get_estimate — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "get_estimate",
            "input": {
              "estimateId": "ESTIMATE_ID"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "get_estimate — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "get_estimate",
          "input": {
            "estimateId": "ESTIMATE_ID"
          }
        }
      }
    },
    {
      "name": "get_estimate_descriptor",
      "kind": "read",
      "description": "Admin-only read. Return one full library descriptor by id (full description body).",
      "x-ops-scopes": [
        "proposals-estimates"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_get_estimate_descriptor"
      },
      "x-examples": [
        {
          "summary": "get_estimate_descriptor — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "get_estimate_descriptor",
            "input": {
              "id": "example",
              "descriptorId": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "get_estimate_descriptor — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "get_estimate_descriptor",
          "input": {
            "id": "example",
            "descriptorId": "example"
          }
        }
      }
    },
    {
      "name": "get_estimate_product",
      "kind": "read",
      "description": "Admin-only read. Return one full product-library doc by id (coverage, price, pds).",
      "x-ops-scopes": [
        "proposals-estimates"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_get_estimate_product"
      },
      "x-examples": [
        {
          "summary": "get_estimate_product — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "get_estimate_product",
            "input": {
              "id": "example",
              "productId": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "get_estimate_product — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "get_estimate_product",
          "input": {
            "id": "example",
            "productId": "example"
          }
        }
      }
    },
    {
      "name": "get_invoice",
      "kind": "read",
      "description": "Owner $ — one invoice plus its payment history (Stripe card + offline check/cash/etc.). Needs the owner-finance scope. Read-only. No orgId.",
      "x-ops-scopes": [
        "owner-finance"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_get_invoice"
      }
    },
    {
      "name": "get_job_production",
      "kind": "read",
      "description": "One job for production: full job record (owner $ fields stripped without Owner $), crew check-ins, progress doc, and shifts on the job. Read-only. No orgId.",
      "x-ops-scopes": [
        "business-data"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_get_job_production"
      }
    },
    {
      "name": "get_lead",
      "kind": "read",
      "description": "Org admin only. Read one full CRM lead by leadId plus its status history (lead.statusHistory + sales lifecycle events). Linked TCPA consent docs are included when smsConsentPath / marketingSmsConsentPath is this org's orgs/{orgId}/consents/sms_ doc. A stored path that fails that allowlist is not opened, and linkedErrors for that key is consent-path-not-allowed. A failed consent read sets linkedErrors to read-failed and the lead body still returns. No orgId.",
      "x-ops-scopes": [
        "leads-crm",
        "business-data"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_get_lead"
      },
      "x-examples": [
        {
          "summary": "get_lead — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "get_lead",
            "input": {
              "leadId": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "get_lead — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "get_lead",
          "input": {
            "leadId": "example"
          }
        }
      }
    },
    {
      "name": "get_production_rate",
      "kind": "read",
      "description": "Admin-only read. Return one full production-rate library doc by id. Call list_production_rates first if you don't have the id.",
      "x-ops-scopes": [
        "production-rates"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_get_production_rate"
      },
      "x-examples": [
        {
          "summary": "get_production_rate — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "get_production_rate",
            "input": {
              "id": "example",
              "rateId": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "get_production_rate — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "get_production_rate",
          "input": {
            "id": "example",
            "rateId": "example"
          }
        }
      }
    },
    {
      "name": "get_settings",
      "kind": "read",
      "description": "Read-only org settings: company profile, contract types, deposit %, invoice settings, estimate rates/tax, production rates, product catalog, descriptors, line-item presets (Numbers settings only with Owner $; secrets never). No orgId.",
      "x-ops-scopes": [
        "business-data"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_get_settings"
      }
    },
    {
      "name": "ingest_receipt",
      "kind": "write",
      "description": "Portal Ops only (Owner $ scope). MUTATING. Store ONE supplier receipt / invoice (any vendor) in the Numbers receipt inbox with its file, and match it to a job. Does NOT post cost — call confirm_receipt after. Safe to repeat: the same receipt returns { duplicate: true, existingId } and never adds a second row (same vendor + invoiceNumber; with no invoice number, same vendor + invoiceDate + amountUsd AND the same file — the same attachment from a second email is still a duplicate). Nothing is dropped on a guess: a different file with the same vendor, date and amount (a repeat purchase), or a near miss (date within a day, total a few cents off or with two digits swapped, vendor name spelled another way) is stored needs_review with possibleDuplicateOf and is never auto-confirmed — a person decides in Numbers. One receipt per call — split a Sherwin daily packet first. Returns receiptId, status (proposed | needs_review), proposedMatch and autoConfirm.eligible. NEVER QuickBooks. NEVER pass orgId.",
      "x-ops-scopes": [
        "owner-finance"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_ingest_receipt"
      },
      "x-examples": [
        {
          "summary": "ingest_receipt — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "ingest_receipt",
            "input": {
              "vendor": "example",
              "invoiceNumber": "example",
              "invoiceDate": "example",
              "amountUsd": 1,
              "taxUsd": 1,
              "storeLocation": "example",
              "po": "example",
              "jobNameHint": "example",
              "addressHint": "example",
              "receivedBy": "example",
              "lineItems": [],
              "fileBase64": "example",
              "fileName": "example",
              "sourceMessageId": "example",
              "sourceInbox": "example",
              "noJob": false
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "ingest_receipt — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "ingest_receipt",
          "input": {
            "vendor": "example",
            "invoiceNumber": "example",
            "invoiceDate": "example",
            "amountUsd": 1,
            "taxUsd": 1,
            "storeLocation": "example",
            "po": "example",
            "jobNameHint": "example",
            "addressHint": "example",
            "receivedBy": "example",
            "lineItems": [],
            "fileBase64": "example",
            "fileName": "example",
            "sourceMessageId": "example",
            "sourceInbox": "example",
            "noJob": false
          }
        }
      }
    },
    {
      "name": "link_recording",
      "kind": "write",
      "description": "Attach a Plaud/Granola/other recording to an estimate via estimateId, or jobId/jobRef (deduped by source+recordingId). Use recordingSearch from estimate_context to pick the visit window, then store {source, recordingId, title, startedAt, url?}. Visit-to-recording matching: recording startedAt should fall within visit start minus 15 minutes through visit end plus 60 minutes. Requires proposals-estimates or jobs-files. Pass dryRun:true to preview without writing. Server verifies the recording id exists (Plaud get_file) before persisting. No orgId.",
      "x-ops-scopes": [
        "proposals-estimates",
        "jobs-files"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_link_recording"
      },
      "x-examples": [
        {
          "summary": "link_recording — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "link_recording",
            "input": {
              "estimateId": "ESTIMATE_ID",
              "jobRef": "example",
              "jobId": "JOB_ID",
              "source": "plaud",
              "recordingId": "example",
              "title": "example",
              "startedAt": 1,
              "url": "example",
              "dryRun": false,
              "otherNote": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "link_recording — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "link_recording",
          "input": {
            "estimateId": "ESTIMATE_ID",
            "jobRef": "example",
            "jobId": "JOB_ID",
            "source": "plaud",
            "recordingId": "example",
            "title": "example",
            "startedAt": 1,
            "url": "example",
            "dryRun": false,
            "otherNote": "example"
          }
        }
      }
    },
    {
      "name": "list_activity",
      "kind": "read",
      "description": "Activity / audit log: staff audit events, Portal Ops assistant calls, and schedule write log, newest first. Read-only, org-scoped, paged (nextCursor). No orgId.",
      "x-ops-scopes": [
        "business-data"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_list_activity"
      }
    },
    {
      "name": "list_app_import_batches",
      "kind": "read",
      "description": "Admin-only read. Recent Import from another app batches for this org (Settings panel and Portal Ops): importBatchId, status (committed / partial / undone), source, mode, counts, via. NEVER pass orgId.",
      "x-ops-scopes": [
        "app-import"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_list_app_import_batches"
      }
    },
    {
      "name": "list_appointments",
      "kind": "read",
      "description": "List booked estimate visits, client visits, and pending estimate offers across jobs (read-only). Use before booking/rescheduling. Filter with estimatorUserId, daysAhead (default 21), sinceMs/untilMs. Production job bars are edit_job_schedule — not returned here.",
      "x-ops-scopes": [
        "scheduling"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_list_appointments"
      },
      "x-examples": [
        {
          "summary": "list_appointments — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "list_appointments",
            "input": {
              "estimatorUserId": "example",
              "daysAhead": 1,
              "sinceMs": 1,
              "untilMs": 1,
              "includePending": false,
              "includeCancelled": false
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "list_appointments — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "list_appointments",
          "input": {
            "estimatorUserId": "example",
            "daysAhead": 1,
            "sinceMs": 1,
            "untilMs": 1,
            "includePending": false,
            "includeCancelled": false
          }
        }
      }
    },
    {
      "name": "list_bank_inbox",
      "kind": "read",
      "description": "Admin-only read. Return uncoded and proposed bank lines for the active org inbox. NEVER pass orgId.",
      "x-ops-scopes": [
        "owner-finance"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_list_bank_inbox"
      }
    },
    {
      "name": "list_cfo_packs",
      "kind": "read",
      "description": "Admin-only. Return the CFO knowledge pack catalog and each pack's purpose. Use before a broad money question if you need to choose the best pack. No orgId is accepted.",
      "x-ops-scopes": [
        "owner-finance"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_list_cfo_packs"
      }
    },
    {
      "name": "list_close_checklist",
      "kind": "read",
      "description": "Admin-only read. Return close-month checklist for the active org (uncoded lines, open recs, unmatched deposits). NEVER pass orgId.",
      "x-ops-scopes": [
        "owner-finance"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_list_close_checklist"
      }
    },
    {
      "name": "list_contacts",
      "kind": "read",
      "description": "Customer contact records (name, email, phone, address, tags, notes). Read-only, org-scoped, paged (nextCursor). No orgId.",
      "x-ops-scopes": [
        "business-data"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_list_contacts"
      }
    },
    {
      "name": "list_crew",
      "kind": "read",
      "description": "Return all crew members (id, name, username, role). No confirm needed — read-only.",
      "x-ops-scopes": [
        "briefing-read",
        "scheduling"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_list_crew"
      }
    },
    {
      "name": "list_drip_sequences",
      "kind": "read",
      "description": "Admin-only. List sales drip automations (built-in + custom) with triggers, cancel events, and steps. Use when admin asks about drips, follow-up sequences, or building automations. No orgId.",
      "x-ops-scopes": [
        "drips-comms"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_list_drip_sequences"
      }
    },
    {
      "name": "list_email_templates",
      "kind": "read",
      "description": "Admin-only. List editable alert email/SMS notification templates (estimate booked, reminders, proposal, invoice, new lead, etc.) with current copy. Use FIRST when admin says edit an email notification, rewrite alert copy, change reminder wording, or Branding email templates. No orgId.",
      "x-ops-scopes": [
        "drips-comms"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_list_email_templates"
      }
    },
    {
      "name": "list_estimate_descriptors",
      "kind": "read",
      "description": "Admin-only read. List the org's descriptor library (PaintScout-style title + description proposal blocks — NOT rooms/substrates). Call BEFORE editing/deleting.",
      "x-ops-scopes": [
        "proposals-estimates"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_list_estimate_descriptors"
      },
      "x-examples": [
        {
          "summary": "list_estimate_descriptors — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "list_estimate_descriptors",
            "input": {
              "query": "example",
              "limit": 1,
              "estimateId": "ESTIMATE_ID"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "list_estimate_descriptors — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "list_estimate_descriptors",
          "input": {
            "query": "example",
            "limit": 1,
            "estimateId": "ESTIMATE_ID"
          }
        }
      }
    },
    {
      "name": "list_estimate_products",
      "kind": "read",
      "description": "Admin-only read. List the org product library (estimateProducts — name, $/gal, coverage, optional PDS). Call BEFORE creating/editing. Distinct from learnedProducts weather cache.",
      "x-ops-scopes": [
        "proposals-estimates"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_list_estimate_products"
      },
      "x-examples": [
        {
          "summary": "list_estimate_products — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "list_estimate_products",
            "input": {
              "query": "example",
              "group": "example",
              "limit": 1
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "list_estimate_products — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "list_estimate_products",
          "input": {
            "query": "example",
            "group": "example",
            "limit": 1
          }
        }
      }
    },
    {
      "name": "list_estimates",
      "kind": "read",
      "description": "Proposals / estimates including drafts and unattached drafts: full areas, substrates, line items, prices, totals, status, sent/viewed/accepted stamps. Read-only, org-scoped, paged (nextCursor). No orgId.",
      "x-ops-scopes": [
        "business-data"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_list_estimates"
      },
      "x-examples": [
        {
          "summary": "list_estimates — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "list_estimates",
            "input": {
              "limit": 1,
              "cursor": "example",
              "sinceMs": 1,
              "untilMs": 1,
              "since": "example",
              "until": "example",
              "jobId": "JOB_ID",
              "contactId": "example",
              "customer": "example",
              "status": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "list_estimates — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "list_estimates",
          "input": {
            "limit": 1,
            "cursor": "example",
            "sinceMs": 1,
            "untilMs": 1,
            "since": "example",
            "until": "example",
            "jobId": "JOB_ID",
            "contactId": "example",
            "customer": "example",
            "status": "example"
          }
        }
      }
    },
    {
      "name": "list_followups",
      "kind": "read",
      "description": "Follow-up / drip status: enrollments, queued and scheduled customer sends, and texting consent records. Read-only, org-scoped, paged (nextCursor). No orgId.",
      "x-ops-scopes": [
        "business-data"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_list_followups"
      }
    },
    {
      "name": "list_hours",
      "kind": "read",
      "description": "Hours: shifts / clock-ins with durations and PTO (users' vacations). Filter by userId, jobId, dates. Wages only with Owner $. Read-only, org-scoped, paged (nextCursor). No orgId.",
      "x-ops-scopes": [
        "business-data"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_list_hours"
      }
    },
    {
      "name": "list_invoices",
      "kind": "read",
      "description": "Owner $ — invoices (deposit / progress / balance) with amountCents, paidCents, owedCents, status, Stripe ids. Needs the owner-finance scope. Read-only, org-scoped, paged (nextCursor). No orgId.",
      "x-ops-scopes": [
        "owner-finance"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_list_invoices"
      }
    },
    {
      "name": "list_job_colors",
      "kind": "read",
      "description": "Colors and products used per job: color plan by room, color notes, and product usage (gallons, product, color). Read-only, org-scoped, paged (nextCursor). No orgId.",
      "x-ops-scopes": [
        "business-data"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_list_job_colors"
      }
    },
    {
      "name": "list_job_costs",
      "kind": "read",
      "description": "Owner $ — job costing per job: costing doc, expenses, and supplier (vendor) invoices with materials. Needs the owner-finance scope. Read-only, org-scoped, paged (nextCursor). No orgId.",
      "x-ops-scopes": [
        "owner-finance"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_list_job_costs"
      }
    },
    {
      "name": "list_jobs",
      "kind": "read",
      "description": "Return all active jobs (id, name, status, address, surface type, schedule, hour estimate). No confirm needed — read-only.",
      "x-ops-scopes": [
        "briefing-read",
        "scheduling",
        "job-status"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_list_jobs"
      },
      "x-examples": [
        {
          "summary": "list_jobs — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "list_jobs",
            "input": {
              "view": "summary",
              "fields": [],
              "compact": false
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "list_jobs — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "list_jobs",
          "input": {
            "view": "summary",
            "fields": [],
            "compact": false
          }
        }
      }
    },
    {
      "name": "list_leads",
      "kind": "read",
      "description": "Org admin only. Website / phone CRM leads at orgs/{orgId}/leads with EVERY stored field (source/referrer, UTM/gclid, landing page, form Q&A, notes, status history, attribution, photos). Not the Sales To-build job list — use list_sales_leads for estimate jobs awaiting proposals. Soft-deleted leads omitted unless includeDeleted. Scans at most 500 rows newest-first per request; truncated is true when the scan cap stops the walk and more docs may exist, and false when the collection ends (including when it ends on the 500th row). Read-only, org-scoped, paged (nextCursor). No orgId.",
      "x-ops-scopes": [
        "leads-crm",
        "business-data"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_list_leads"
      },
      "x-examples": [
        {
          "summary": "list_leads — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "list_leads",
            "input": {
              "limit": 1,
              "cursor": "example",
              "sinceMs": 1,
              "untilMs": 1,
              "since": "example",
              "until": "example",
              "jobId": "JOB_ID",
              "contactId": "example",
              "customer": "example",
              "status": "example",
              "includeDeleted": false
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "list_leads — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "list_leads",
          "input": {
            "limit": 1,
            "cursor": "example",
            "sinceMs": 1,
            "untilMs": 1,
            "since": "example",
            "until": "example",
            "jobId": "JOB_ID",
            "contactId": "example",
            "customer": "example",
            "status": "example",
            "includeDeleted": false
          }
        }
      }
    },
    {
      "name": "list_learning_ai",
      "kind": "read",
      "description": "Read-only health of all chat-learning AI surfaces (Pro Tips facts, Estimate AI style, share Ask FAQs + document misses). Cursor agents adjust those systems; Grok Bot / ops brief uses this to check up. Counts and sanitized miss hints only — no raw chats, emails, or phones.",
      "x-ops-scopes": [
        "briefing-read"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_list_learning_ai"
      }
    },
    {
      "name": "list_messages",
      "kind": "read",
      "description": "Every customer email and text sent or received (email threads, SMS threads, job message timeline, outbound email sends) with delivery status. Read-only, org-scoped, paged (nextCursor). No orgId.",
      "x-ops-scopes": [
        "business-data"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_list_messages"
      }
    },
    {
      "name": "list_payments",
      "kind": "read",
      "description": "Owner $ — payment history across invoices (Stripe + offline), newest first, filterable by payment date. Needs the owner-finance scope. Read-only, org-scoped, paged (nextCursor). No orgId.",
      "x-ops-scopes": [
        "owner-finance"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_list_payments"
      }
    },
    {
      "name": "list_pending_checkins",
      "kind": "read",
      "description": "Return pending daily check-ins awaiting admin/lead review. No confirm needed — read-only. Use when the admin asks what check-ins need approved, or before publishing if the pending check-in IDs are not already visible in the PENDING CHECK-INS block.",
      "x-ops-scopes": [
        "action-feed"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_list_pending_checkins"
      }
    },
    {
      "name": "list_pending_share_photos",
      "kind": "read",
      "description": "Photos waiting for staff approval. Same private record pipeline as list_photos. Read-only, org-scoped, paged (nextCursor). No orgId.",
      "x-ops-scopes": [
        "photos"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_list_pending_share_photos"
      }
    },
    {
      "name": "list_photos",
      "kind": "read",
      "description": "Estimate, job, check-in, and assistant photos with metadata: who, when, room/area, caption, and whether the customer can see it (assistant photos show their staff approval state). Read-only, org-scoped, paged (nextCursor). No orgId.",
      "x-ops-scopes": [
        "business-data",
        "photos"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_list_photos"
      }
    },
    {
      "name": "list_production",
      "kind": "read",
      "description": "Jobs / production: status, schedule dates, crew assignees and day spots, hours estimate, delays. Read-only, org-scoped, paged (nextCursor). No orgId.",
      "x-ops-scopes": [
        "business-data"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_list_production"
      }
    },
    {
      "name": "list_production_rates",
      "kind": "read",
      "description": "Admin-only read. List the org's production-rate library (sqft/hr, lnft/hr, hrs/item, $/sqft, $/item) used by Estimate Builder. Call BEFORE editing/deleting. Optional query/group filter. This is NOT Numbers burden and NOT billable hourlyRate.",
      "x-ops-scopes": [
        "production-rates"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_list_production_rates"
      }
    },
    {
      "name": "list_proposals",
      "kind": "read",
      "description": "Sales → Proposals hub list with state new/draft/sent/accepted/declined, including unattached drafts. filter=draft for Crosshair drafts. Not list_estimate_* library tools. No orgId.",
      "x-ops-scopes": [
        "proposals-estimates"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_list_proposals"
      },
      "x-examples": [
        {
          "summary": "list_proposals — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "list_proposals",
            "input": {
              "view": "summary",
              "fields": [],
              "compact": false,
              "filter": "draft",
              "search": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "list_proposals — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "list_proposals",
          "input": {
            "view": "summary",
            "fields": [],
            "compact": false,
            "filter": "draft",
            "search": "example"
          }
        }
      }
    },
    {
      "name": "list_receipts",
      "kind": "read",
      "description": "Portal Ops only (Owner $ scope). Read the receipt inbox. status: pending (default — uploaded, proposed, needs_review), needs_review (the review queue), proposed, uploaded, confirmed, dismissed, all. Each row has the proposed job, confidence, top candidates and autoConfirm.eligible. NEVER pass orgId.",
      "x-ops-scopes": [
        "owner-finance"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_list_receipts"
      }
    },
    {
      "name": "list_recordings",
      "kind": "read",
      "description": "Find recordings in the active org's connected Plaud or Granola account. Paginated metadata only; use estimate_context recordingSearch visit window and keywords to select the right recording, read_recording_transcript before estimating, then link_recording to pair it to the estimate/job. No personal-account browsing. Requires proposals-estimates. No orgId.",
      "x-ops-scopes": [
        "proposals-estimates"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_list_recordings"
      },
      "x-examples": [
        {
          "summary": "Find visit recordings",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "list_recordings",
            "input": {
              "source": "plaud",
              "page": 1
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "Find visit recordings",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "list_recordings",
          "input": {
            "source": "plaud",
            "page": 1
          }
        }
      }
    },
    {
      "name": "list_recurring_reminders",
      "kind": "read",
      "description": "Return every recurring reminder already configured, with id, title, body, schedule, targets, enabled flag. No confirm — read-only. Call this BEFORE edit_recurring_reminder / delete_recurring_reminder / toggle_recurring_reminder so you have the correct id. Also call it when admin asks 'what reminders do I have set up' or similar.",
      "x-ops-scopes": [
        "briefing-read",
        "scheduling"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_list_recurring_reminders"
      }
    },
    {
      "name": "list_reviews_referrals",
      "kind": "read",
      "description": "Referral leads (who referred, code), review-request enrollments, review fields on jobs, and the org's Google review link. Read-only, org-scoped, paged (nextCursor). No orgId.",
      "x-ops-scopes": [
        "business-data"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_list_reviews_referrals"
      }
    },
    {
      "name": "list_sales_leads",
      "kind": "read",
      "description": "Sales → Proposals To-build list. Estimate-status jobs with no estimateId yet (hub state new). Not the product/descriptor library. No orgId.",
      "x-ops-scopes": [
        "leads-crm"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_list_sales_leads"
      },
      "x-examples": [
        {
          "summary": "list_sales_leads — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "list_sales_leads",
            "input": {
              "search": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "list_sales_leads — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "list_sales_leads",
          "input": {
            "search": "example"
          }
        }
      }
    },
    {
      "name": "list_schedule",
      "kind": "read",
      "description": "Estimator calendars: each estimator's hours, booked estimate + client visits, and open slots (Portal bookings + estimator hours; Google busy blocks not included). Read-only, org-scoped, paged (nextCursor). No orgId.",
      "x-ops-scopes": [
        "scheduling",
        "business-data"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_list_schedule"
      }
    },
    {
      "name": "list_staff",
      "kind": "read",
      "description": "Staff roster: roles, titles, permissions, contact info (pay rates only with Owner $; tokens never). Read-only, org-scoped, paged (nextCursor). No orgId.",
      "x-ops-scopes": [
        "business-data"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_list_staff"
      }
    },
    {
      "name": "list_teams",
      "kind": "read",
      "description": "Return every team with its id, name, and members. No confirm — read-only. Call this before assign_to_job if you need to look up a team id.",
      "x-ops-scopes": [
        "briefing-read",
        "scheduling"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_list_teams"
      }
    },
    {
      "name": "list_tools",
      "kind": "read",
      "description": "Return every tool in the shop inventory (id, name, category, status, which job it's at if any). No confirm needed — read-only. Call this BEFORE any tool-editing action so you have the right toolId.",
      "x-ops-scopes": [
        "briefing-read"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_list_tools"
      }
    },
    {
      "name": "list_webhooks",
      "kind": "read",
      "description": "List this org's Portal Ops webhooks (URL, events, last delivery). Never returns signing secrets. No orgId.",
      "x-ops-scopes": [
        "webhooks"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_list_webhooks"
      }
    },
    {
      "name": "mark_tool_fixed",
      "kind": "write",
      "description": "Mark a previously broken tool as fixed and available again.",
      "x-ops-scopes": [
        "company-state"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_mark_tool_fixed"
      },
      "x-examples": [
        {
          "summary": "mark_tool_fixed — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "mark_tool_fixed",
            "input": {
              "toolId": "example",
              "byName": "example",
              "note": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "mark_tool_fixed — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "mark_tool_fixed",
          "input": {
            "toolId": "example",
            "byName": "example",
            "note": "example"
          }
        }
      }
    },
    {
      "name": "mark_tool_repair",
      "kind": "write",
      "description": "Mark a tool as needing repair. Use when someone says 'X is broken', 'sprayer is acting up', 'ladder rung cracked'.",
      "x-ops-scopes": [
        "company-state"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_mark_tool_repair"
      },
      "x-examples": [
        {
          "summary": "mark_tool_repair — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "mark_tool_repair",
            "input": {
              "toolId": "example",
              "byName": "example",
              "note": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "mark_tool_repair — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "mark_tool_repair",
          "input": {
            "toolId": "example",
            "byName": "example",
            "note": "example"
          }
        }
      }
    },
    {
      "name": "match_bank_line",
      "kind": "write",
      "description": "Admin-only MUTATING. Propose a job, invoice, or category match for one bank transaction. Does NOT post job expenses. NEVER pass orgId.",
      "x-ops-scopes": [
        "owner-finance"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_match_bank_line"
      },
      "x-examples": [
        {
          "summary": "match_bank_line — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "match_bank_line",
            "input": {
              "txnId": "example",
              "jobId": "JOB_ID",
              "invoiceId": "example",
              "category": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "match_bank_line — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "match_bank_line",
          "input": {
            "txnId": "example",
            "jobId": "JOB_ID",
            "invoiceId": "example",
            "category": "example"
          }
        }
      }
    },
    {
      "name": "offer_estimate_visit",
      "kind": "write",
      "description": "Store a pending estimate visit offer on job.pendingEstimateVisit (customer has not accepted). Same slot shape as book_estimate_visit. Staff Send / share Accept promotes to estimateAppointment.",
      "x-ops-scopes": [
        "scheduling"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_offer_estimate_visit"
      },
      "x-examples": [
        {
          "summary": "offer_estimate_visit — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "offer_estimate_visit",
            "input": {
              "jobId": "JOB_ID",
              "jobName": "example",
              "startAt": 1,
              "endAt": 1,
              "estimatorUserId": "example",
              "estimatorName": "example",
              "notes": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "offer_estimate_visit — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "offer_estimate_visit",
          "input": {
            "jobId": "JOB_ID",
            "jobName": "example",
            "startAt": 1,
            "endAt": 1,
            "estimatorUserId": "example",
            "estimatorName": "example",
            "notes": "example"
          }
        }
      }
    },
    {
      "name": "polish_delay_reason",
      "kind": "read",
      "description": "Preview playful customer-facing delay copy (same as check-in Delay polish). Does not post delay — use with post_job_delay reason field. No orgId.",
      "x-ops-scopes": [
        "scheduling",
        "job-delays"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_polish_delay_reason"
      },
      "x-examples": [
        {
          "summary": "polish_delay_reason — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "polish_delay_reason",
            "input": {
              "jobId": "JOB_ID",
              "jobName": "example",
              "kind": "example",
              "reason": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "polish_delay_reason — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "polish_delay_reason",
          "input": {
            "jobId": "JOB_ID",
            "jobName": "example",
            "kind": "example",
            "reason": "example"
          }
        }
      }
    },
    {
      "name": "polish_limbo_reason",
      "kind": "read",
      "description": "Preview rewritten INTERNAL limbo reason (staff note — same as job status UI “Rewrite with AI”). Does not save — pass result to set_job_status limboReason. No orgId.",
      "x-ops-scopes": [
        "job-status"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_polish_limbo_reason"
      },
      "x-examples": [
        {
          "summary": "polish_limbo_reason — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "polish_limbo_reason",
            "input": {
              "jobId": "JOB_ID",
              "jobName": "example",
              "reason": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "polish_limbo_reason — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "polish_limbo_reason",
          "input": {
            "jobId": "JOB_ID",
            "jobName": "example",
            "reason": "example"
          }
        }
      }
    },
    {
      "name": "post_job_delay",
      "kind": "write",
      "description": "Post a schedule delay on a job (same as check-in Delay / Action Feed): pushes schedule.endDate (default +1 workday), writes customer share progressLog + job.weatherDelays, and optionally cascades follow-on exterior jobs on the same crew (rain/heat). Use when admin texts 'rain delay on 1253', 'push Johnson job to Friday', 'heat delay — cascade the crew queue'.",
      "x-ops-scopes": [
        "scheduling",
        "job-delays"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_post_job_delay"
      },
      "x-examples": [
        {
          "summary": "post_job_delay — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "post_job_delay",
            "input": {
              "jobId": "JOB_ID",
              "jobName": "example",
              "newEndDate": "example",
              "delayKind": "rain",
              "reason": "example",
              "cascade": false,
              "date": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "post_job_delay — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "post_job_delay",
          "input": {
            "jobId": "JOB_ID",
            "jobName": "example",
            "newEndDate": "example",
            "delayKind": "rain",
            "reason": "example",
            "cascade": false,
            "date": "example"
          }
        }
      }
    },
    {
      "name": "preview_app_import",
      "kind": "write",
      "description": "Admin-only WRITE. Stages a preview for Import from another app (PaintScout export or spreadsheet). Matches against this org's jobs (quote number, never invoice number), contacts (email / phone / PaintScout id), estimates, and invoices. Returns counts, duplicates (already in Portal), sample rows, and a previewId. Writes no contacts, jobs, estimates, or invoices — it does write the staged preview, which expires in 24 hours. Committing that preview keeps edits made after it was staged. Upload cap 4 MB (tar.gz unpacked cap 32 MB). Unknown source is bad-source. source dripjobs is coming-soon. Unknown mode is bad-mode. A plan that cannot be built is bad-plan. A record that fails validation is bad-payload. Never contacts a customer. NEVER pass orgId.",
      "x-ops-scopes": [
        "app-import"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_preview_app_import"
      },
      "x-examples": [
        {
          "summary": "preview_app_import — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "preview_app_import",
            "input": {
              "source": "paintscout",
              "mode": "unresolved-jobs",
              "tarGzBase64": "example",
              "files": {},
              "csv": "example",
              "quoteNumbers": [],
              "recordIds": [],
              "query": "example",
              "status": "example",
              "maxRows": 1
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "preview_app_import — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "preview_app_import",
          "input": {
            "source": "paintscout",
            "mode": "unresolved-jobs",
            "tarGzBase64": "example",
            "files": {},
            "csv": "example",
            "quoteNumbers": [],
            "recordIds": [],
            "query": "example",
            "status": "example",
            "maxRows": 1
          }
        }
      }
    },
    {
      "name": "publish_checkins",
      "kind": "write",
      "description": "Approve/publish one or more pending daily check-ins to the customer share page. Use when admin says 'approve check-ins', 'publish these check-ins', or similar. Photo re-draft is no longer available — if they want rewrite/re-draft, tell them to edit the three customer fields on the Confirm card (or Separate/Polish text) before publishing. The Confirm card is the review step; do not ask for separate permission when the requested scope is clear.",
      "x-ops-scopes": [
        "action-feed"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_publish_checkins"
      },
      "x-examples": [
        {
          "summary": "publish_checkins — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "publish_checkins",
            "input": {
              "checkinIds": [],
              "jobId": "JOB_ID",
              "allPending": false,
              "reason": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "publish_checkins — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "publish_checkins",
          "input": {
            "checkinIds": [],
            "jobId": "JOB_ID",
            "allPending": false,
            "reason": "example"
          }
        }
      }
    },
    {
      "name": "query_cfo_knowledge",
      "kind": "read",
      "description": "Admin-only. Query one CFO knowledge pack over the active org's Numbers, jobs, shifts, estimates, invoices, and optional expenses. Use for specific money questions about month performance, job costing, budget variance, job type, crew costing, hour-budget KPIs (who was on jobs / under hours), sales pulse, AR/cash, underpriced work, or advisor actions. Always cite the returned basis/footnotes; never invent money values. No orgId is accepted.",
      "x-ops-scopes": [
        "owner-finance"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_query_cfo_knowledge"
      }
    },
    {
      "name": "read_estimate_handoff",
      "kind": "read",
      "description": "CHECK FIRST before working on any estimate: read internal context left by staff and earlier agents, plus linked recordings and attached meeting-note summaries. Use estimateId, jobId, or jobRef. Notes are visible in the estimator's Office handoff, never to the client. Requires proposals-estimates. After working, call add_estimate_note with progress, decisions, open questions, and next steps. No orgId.",
      "x-ops-scopes": [
        "proposals-estimates"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_read_estimate_handoff"
      },
      "x-examples": [
        {
          "summary": "Check prior context before estimating",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "read_estimate_handoff",
            "input": {
              "jobId": "job-123"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "Check prior context before estimating",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "read_estimate_handoff",
          "input": {
            "jobId": "job-123"
          }
        }
      }
    },
    {
      "name": "read_home_file",
      "kind": "read",
      "description": "Portal Ops API. Read a customer's Home File: a timeline of their jobs, newest first. Each job has the job number (the number staff give the paint store to look up the order), job dates, crew, warranty end date, paint colors by area with exact formulas (SW number, sheen, product line, custom-match formula), products with dry / cure times, surface and condition notes, the leftover paint log, before / after photo pairs, final photos, and staff follow-ups (Home File ready to send, ~1 year touch-up check — reminders only, nothing is sent). Pass contactId for one customer or an array for several, or jobId (job id or job number, one or an array) to read the Home File of that job's customer. Read-only. NEVER returns prices, invoices or proposals. Gate code, lockbox, access notes and pets are included only for keys that can also call set_home_file_access. NEVER pass orgId.",
      "x-ops-scopes": [
        "home-file"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_read_home_file"
      }
    },
    {
      "name": "read_job",
      "kind": "read",
      "description": "Return a job's full record: address, surface type (interior/exterior/mixed), schedule, summary, products, memory notes, work-order scope, room colors (colorPlan), fixed-price paymentSchedule + contractTypeId when set, and customer info. Use when you need detail beyond the CURRENT JOBS block.",
      "x-ops-scopes": [
        "briefing-read",
        "scheduling",
        "job-status"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_read_job"
      },
      "x-examples": [
        {
          "summary": "read_job — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "read_job",
            "input": {
              "view": "summary",
              "fields": [],
              "compact": false,
              "jobId": "JOB_ID"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "read_job — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "read_job",
          "input": {
            "view": "summary",
            "fields": [],
            "compact": false,
            "jobId": "JOB_ID"
          }
        }
      }
    },
    {
      "name": "read_proposal",
      "kind": "read",
      "description": "Read one proposal plus its job: customer, address, rooms (with L×W×H), line items, selected descriptors (id/name/description), substrate photos, totals, last touched, missing fields. Pass estimateId or jobId. No orgId.",
      "x-ops-scopes": [
        "proposals-estimates"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_read_proposal"
      },
      "x-examples": [
        {
          "summary": "read_proposal — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "read_proposal",
            "input": {
              "view": "summary",
              "fields": [],
              "compact": false,
              "estimateId": "ESTIMATE_ID",
              "jobId": "JOB_ID"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "read_proposal — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "read_proposal",
          "input": {
            "view": "summary",
            "fields": [],
            "compact": false,
            "estimateId": "ESTIMATE_ID",
            "jobId": "JOB_ID"
          }
        }
      }
    },
    {
      "name": "read_recording_transcript",
      "kind": "read",
      "description": "Read a Plaud/Granola recording transcript from the active org's connected account. Use source and recordingId from list_recordings or read_estimate_handoff. Returns transcript separately from summary; transcriptAvailable:false means no transcript yet, never infer missing speech from a summary. Follow nextOffset until null to read all transcript pages. Staff-only. Requires proposals-estimates. No personal-account browsing or orgId.",
      "x-ops-scopes": [
        "proposals-estimates"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_read_recording_transcript"
      },
      "x-examples": [
        {
          "summary": "Read recording speech",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "read_recording_transcript",
            "input": {
              "source": "plaud",
              "recordingId": "recording-123",
              "offset": 0
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "Read recording speech",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "read_recording_transcript",
          "input": {
            "source": "plaud",
            "recordingId": "recording-123",
            "offset": 0
          }
        }
      }
    },
    {
      "name": "record_collected_payment",
      "kind": "write",
      "description": "Finance-only MUTATING tool. Record ONE payment the company has ALREADY collected (e.g. taken in PaintScout) as a paid deposit or custom invoice on a job. Sends NOTHING to the customer — no email, text, receipt, or Stripe. If a live payment with the same job, amount, date and type already exists, it returns needsConfirm:true and saves nothing — ask the admin, and only if they say it is a separate payment call again with confirmSeparate:true. Use only with an amount, date and method the admin gave you. NEVER invent money. NEVER pass orgId. Confirm card writes.",
      "x-ops-scopes": [
        "owner-finance"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_record_collected_payment"
      },
      "x-examples": [
        {
          "summary": "record_collected_payment — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "record_collected_payment",
            "input": {
              "jobId": "JOB_ID",
              "kind": "deposit",
              "amount": 1,
              "method": "check",
              "date": "example",
              "reference": "example",
              "checkNumber": "example",
              "note": "example",
              "confirmSeparate": false
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "record_collected_payment — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "record_collected_payment",
          "input": {
            "jobId": "JOB_ID",
            "kind": "deposit",
            "amount": 1,
            "method": "check",
            "date": "example",
            "reference": "example",
            "checkNumber": "example",
            "note": "example",
            "confirmSeparate": false
          }
        }
      }
    },
    {
      "name": "remember",
      "kind": "write",
      "description": "Append a fact to the admin assistant long-term company lore. Use when admin says 'remember X', 'learn X', 'from now on X', or describes a policy/preference that should apply across future jobs and conversations. Keep facts short and declarative.",
      "x-ops-scopes": [
        "company-state"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_remember"
      },
      "x-examples": [
        {
          "summary": "remember — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "remember",
            "input": {
              "fact": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "remember — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "remember",
          "input": {
            "fact": "example"
          }
        }
      }
    },
    {
      "name": "remove_job_file",
      "kind": "write",
      "description": "Alias of delete_job_file. Detach ONE file from an existing job's employee-facing files list (same as Photos remove). Pass jobId plus storagePath (preferred), fileName, or fileId. Exact match only — does not delete other files. Use to remove estimate/raster PDFs while keeping TEXT work orders. Aliases: remove_job_file, detach_job_file.",
      "x-ops-scopes": [
        "jobs-files"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_remove_job_file"
      },
      "x-examples": [
        {
          "summary": "remove_job_file — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "remove_job_file",
            "input": {
              "jobId": "JOB_ID",
              "storagePath": "example",
              "path": "example",
              "filePath": "example",
              "fileName": "example",
              "name": "example",
              "fileId": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "remove_job_file — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "remove_job_file",
          "input": {
            "jobId": "JOB_ID",
            "storagePath": "example",
            "path": "example",
            "filePath": "example",
            "fileName": "example",
            "name": "example",
            "fileId": "example"
          }
        }
      }
    },
    {
      "name": "remove_spot_assignment",
      "kind": "write",
      "description": "Remove an existing spot (day-level) assignment from a job. Preferred: pass the spotId shown in square brackets after the job's 'spot:' line in the CURRENT JOBS block. Alternatively pass userName (and optionally a day) to drop that person's spot on the job. Use when the admin says 'never mind, take Seth off 1212 tomorrow', 'cancel Ben's spot on the Johnson job'. Does NOT touch the job's schedule dates or regular assignees.",
      "x-ops-scopes": [
        "scheduling"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_remove_spot_assignment"
      },
      "x-examples": [
        {
          "summary": "remove_spot_assignment — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "remove_spot_assignment",
            "input": {
              "jobId": "JOB_ID",
              "jobName": "example",
              "spotId": "example",
              "userName": "example",
              "start": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "remove_spot_assignment — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "remove_spot_assignment",
          "input": {
            "jobId": "JOB_ID",
            "jobName": "example",
            "spotId": "example",
            "userName": "example",
            "start": "example"
          }
        }
      }
    },
    {
      "name": "rename_job",
      "kind": "write",
      "description": "Rename an existing job's title to the PaintScout PO/quote number (e.g. Kim Brandt → name '1253'). Optional contact fields only (customerName, phone, email, address). Does NOT change status, hours, billing, assignees, or send anything. Prefer this over edit_job when fixing a wrong customer-name title. Alias: set_job_name.",
      "x-ops-scopes": [
        "jobs-files"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_rename_job"
      },
      "x-examples": [
        {
          "summary": "rename_job — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "rename_job",
            "input": {
              "jobId": "JOB_ID",
              "name": "example",
              "jobNumber": "example",
              "poNumber": "example",
              "quoteNumber": "example",
              "customerName": "example",
              "address": "example",
              "phone": "5735550100",
              "customerPhone": "5735550100",
              "email": "example",
              "customerEmail": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "rename_job — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "rename_job",
          "input": {
            "jobId": "JOB_ID",
            "name": "example",
            "jobNumber": "example",
            "poNumber": "example",
            "quoteNumber": "example",
            "customerName": "example",
            "address": "example",
            "phone": "5735550100",
            "customerPhone": "5735550100",
            "email": "example",
            "customerEmail": "example"
          }
        }
      }
    },
    {
      "name": "reopen_estimate_draft",
      "kind": "write",
      "description": "Alias of reopen_proposal_draft. Reopen a SENT proposal to draft so save_proposal_draft can edit it (scope cut). Clears estimate status/sentAt, frozen presentation, and job proposalSent mirrors. Does NOT send/share/publish. Does NOT reopen accepted/signed (use change orders). Idempotent if already an editable draft. Pass estimateId or jobId. Aliases: reopen_estimate_draft, unsend_proposal_to_draft. No orgId.",
      "x-ops-scopes": [
        "proposals-estimates"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_reopen_estimate_draft"
      },
      "x-examples": [
        {
          "summary": "reopen_estimate_draft — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "reopen_estimate_draft",
            "input": {
              "estimateId": "ESTIMATE_ID",
              "jobId": "JOB_ID"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "reopen_estimate_draft — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "reopen_estimate_draft",
          "input": {
            "estimateId": "ESTIMATE_ID",
            "jobId": "JOB_ID"
          }
        }
      }
    },
    {
      "name": "reopen_proposal_draft",
      "kind": "write",
      "description": "Reopen a SENT proposal to draft so save_proposal_draft can edit it (scope cut). Clears estimate status/sentAt, frozen presentation, and job proposalSent mirrors. Does NOT send/share/publish. Does NOT reopen accepted/signed (use change orders). Idempotent if already an editable draft. Pass estimateId or jobId. Aliases: reopen_estimate_draft, unsend_proposal_to_draft. No orgId.",
      "x-ops-scopes": [
        "proposals-estimates"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_reopen_proposal_draft"
      },
      "x-examples": [
        {
          "summary": "reopen_proposal_draft — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "reopen_proposal_draft",
            "input": {
              "estimateId": "ESTIMATE_ID",
              "jobId": "JOB_ID"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "reopen_proposal_draft — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "reopen_proposal_draft",
          "input": {
            "estimateId": "ESTIMATE_ID",
            "jobId": "JOB_ID"
          }
        }
      }
    },
    {
      "name": "research_estimate_product",
      "kind": "read",
      "description": "Admin-only read. Research manufacturer PDS thresholds for a product name (does NOT save). Returns a draft with pds/productKey. Always ASK the admin whether to save to the product library before calling upsert_estimate_product. Never invent $/gal or coverage — use values the admin stated.",
      "x-ops-scopes": [
        "proposals-estimates"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_research_estimate_product"
      },
      "x-examples": [
        {
          "summary": "research_estimate_product — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "research_estimate_product",
            "input": {
              "query": "example",
              "name": "example",
              "brand": "example",
              "line": "example",
              "sheen": "example",
              "finish": "example",
              "group": "example",
              "pricePerUnit": 1,
              "coveragePerUnit": 1
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "research_estimate_product — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "research_estimate_product",
          "input": {
            "query": "example",
            "name": "example",
            "brand": "example",
            "line": "example",
            "sheen": "example",
            "finish": "example",
            "group": "example",
            "pricePerUnit": 1,
            "coveragePerUnit": 1
          }
        }
      }
    },
    {
      "name": "resend_proposal_email",
      "kind": "write",
      "description": "Email a copy of an ALREADY-SENT proposal to ONE extra email address (e.g. a spouse). Sends the same customer proposal email with the SAME existing share link. EMAIL ONLY — never SMS. Never reopens, reprices, refreezes, restamps, or changes the proposal, its price, sent date, share link, or the customer's email on file. One recipient per call; the same address is refused within 24h unless the proposal was revised and re-sent. Refused (nothing sent) if the email would show a gate / door / lockbox code — tell the user to fix the job address or name. 20 copies per hour per key, 60 per org. If it returns copy-may-have-sent, tell the user it may have gone out and do NOT retry. Logged on the proposal's send history (who, when, to whom). Pass estimateId or jobId plus email. No orgId.",
      "x-ops-scopes": [
        "proposals-estimates"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_resend_proposal_email"
      },
      "x-examples": [
        {
          "summary": "Email a copy of a SENT proposal to one extra address (email only, same link)",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "resend_proposal_email",
            "input": {
              "estimateId": "ESTIMATE_ID",
              "email": "second.customer@example.com"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "Email a copy of a SENT proposal to one extra address (email only, same link)",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "resend_proposal_email",
          "input": {
            "estimateId": "ESTIMATE_ID",
            "email": "second.customer@example.com"
          }
        }
      }
    },
    {
      "name": "restore_proposal_revision",
      "kind": "write",
      "description": "Restore an estimate draft from a prior revision snapshot (orgs/{orgId}/estimates/{id}/revisions). Requires estimateId (or jobId) and revisionId from the revision history. Optional ifVersion for optimistic concurrency. Does NOT send/share.",
      "x-ops-scopes": [
        "proposals-estimates"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_restore_proposal_revision"
      },
      "x-examples": [
        {
          "summary": "restore_proposal_revision — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "restore_proposal_revision",
            "input": {
              "estimateId": "ESTIMATE_ID",
              "jobId": "JOB_ID",
              "revisionId": "example",
              "ifVersion": 1,
              "dryRun": false,
              "idempotencyKey": "example",
              "returnView": "summary"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "restore_proposal_revision — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "restore_proposal_revision",
          "input": {
            "estimateId": "ESTIMATE_ID",
            "jobId": "JOB_ID",
            "revisionId": "example",
            "ifVersion": 1,
            "dryRun": false,
            "idempotencyKey": "example",
            "returnView": "summary"
          }
        }
      }
    },
    {
      "name": "save_annotated_photo",
      "kind": "write",
      "description": "Save an AI-annotated version of a photo as a NEW linked photo (parentPhotoId / parentStoragePath). The original is never modified. The copy is staff-only until approved in Portal. No orgId.",
      "x-ops-scopes": [
        "photos"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_save_annotated_photo"
      },
      "x-examples": [
        {
          "summary": "save_annotated_photo — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "save_annotated_photo",
            "input": {
              "sourcePhotoId": "example",
              "jobId": "JOB_ID",
              "sourceStoragePath": "example",
              "sourceUrl": "example",
              "imageBase64": "example",
              "contentType": "example",
              "caption": "example",
              "room": "example",
              "annotationNotes": "example",
              "fileName": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "save_annotated_photo — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "save_annotated_photo",
          "input": {
            "sourcePhotoId": "example",
            "jobId": "JOB_ID",
            "sourceStoragePath": "example",
            "sourceUrl": "example",
            "imageBase64": "example",
            "contentType": "example",
            "caption": "example",
            "room": "example",
            "annotationNotes": "example",
            "fileName": "example"
          }
        }
      }
    },
    {
      "name": "save_estimate_settings",
      "kind": "write",
      "description": "Admin-only MUTATING. Write org estimate settings (billable labor hourlyRate, taxRate, wastePercent, showSubstratePriceDescriptors, packagePricingEnabled, defaultProductionRates). This is NOT Numbers burden. Use when admin wants to change the sell $/hr or the default production rate for walls/ceiling/etc. For one slot, pass slot + productionRateId (empty id clears).",
      "x-ops-scopes": [
        "proposals-estimates"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_save_estimate_settings"
      },
      "x-examples": [
        {
          "summary": "save_estimate_settings — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "save_estimate_settings",
            "input": {
              "hourlyRate": 1,
              "taxRate": 1,
              "wastePercent": 1,
              "showSubstratePriceDescriptors": false,
              "packagePricingEnabled": false,
              "defaultProductionRates": {},
              "slot": "walls",
              "productionRateId": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "save_estimate_settings — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "save_estimate_settings",
          "input": {
            "hourlyRate": 1,
            "taxRate": 1,
            "wastePercent": 1,
            "showSubstratePriceDescriptors": false,
            "packagePricingEnabled": false,
            "defaultProductionRates": {},
            "slot": "walls",
            "productionRateId": "example"
          }
        }
      }
    },
    {
      "name": "save_job_costing",
      "kind": "write",
      "description": "Admin-only MUTATING tool. Write sold contract $ and/or all-in project cost for a job into Numbers job costing (orgs/{orgId}/jobCosting/{jobId}). Use when the admin gives a contract/invoice/sold amount so GP calculates — especially WO-only jobs without an in-app estimate. soldUsd feeds CFO sold basis (uploaded_estimate); totalCostUsd when set makes GP = sold − total. NEVER stash dollars in edit_summary. NEVER invent money. NEVER pass orgId. Confirm card writes.",
      "x-ops-scopes": [
        "owner-finance"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_save_job_costing"
      },
      "x-examples": [
        {
          "summary": "save_job_costing — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "save_job_costing",
            "input": {
              "jobId": "JOB_ID",
              "soldUsd": 1,
              "totalCostUsd": 1,
              "note": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "save_job_costing — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "save_job_costing",
          "input": {
            "jobId": "JOB_ID",
            "soldUsd": 1,
            "totalCostUsd": 1,
            "note": "example"
          }
        }
      }
    },
    {
      "name": "save_numbers_settings",
      "kind": "write",
      "description": "Admin-only MUTATING tool. Write Numbers/CFO settings for the active org (burden, overhead, owner pay, margin target, budgets, wage samples). ALWAYS use when admin wants settings filled in — Confirm card is the review step. If wages are provided without burdenRatePerHour, client suggests burden automatically. NEVER invent money values. NEVER pass orgId.",
      "x-ops-scopes": [
        "owner-finance"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_save_numbers_settings"
      },
      "x-examples": [
        {
          "summary": "save_numbers_settings — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "save_numbers_settings",
            "input": {
              "burdenRatePerHour": 1,
              "burdenLoadFactor": 1,
              "monthlyOverheadUsd": 1,
              "ownerPayUsd": 1,
              "targetMarginPct": 1,
              "monthlyProducedTargetUsd": 1,
              "monthlySoldTargetUsd": 1,
              "monthlyMaterialsBudgetUsd": 1,
              "monthlyLaborBudgetHours": 1,
              "budgetAlertPct": 1,
              "wageSamples": [],
              "applySuggestedBurden": false
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "save_numbers_settings — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "save_numbers_settings",
          "input": {
            "burdenRatePerHour": 1,
            "burdenLoadFactor": 1,
            "monthlyOverheadUsd": 1,
            "ownerPayUsd": 1,
            "targetMarginPct": 1,
            "monthlyProducedTargetUsd": 1,
            "monthlySoldTargetUsd": 1,
            "monthlyMaterialsBudgetUsd": 1,
            "monthlyLaborBudgetHours": 1,
            "budgetAlertPct": 1,
            "wageSamples": [],
            "applySuggestedBurden": false
          }
        }
      }
    },
    {
      "name": "save_proposal_draft",
      "kind": "write",
      "description": "Persist edits on an EXISTING draft proposal only — same fields as Estimate Builder (areas with L×W×H, lineItems/descriptors, notes, hours, pricing, substrates). Attach catalog descriptors via descriptorIds (from list_estimate_descriptors). Does NOT send/share/publish. Does NOT create a job or a new estimate. Does NOT mint library descriptors. Requires estimateId or a job that already has estimateId. PRICING RULE: price painting and staining through areas[] (substrates with productId, productionRateId, coats) so the crew work order gets hours and materials. Use a flat-priced lineItem only for an add-on such as lift or equipment rental (set flatAddOn:true). Never add a separate paint/stain materials line; materials come from the products on area substrates. GOOD: areas:[{name:\"Living room\",length:18,width:15,height:9,substrates:[{name:\"Walls\",productionRateId,productId,coats:2}]}] plus lineItems:[{name:\"Boom lift rental\",amount:1200,flatAddOn:true}]. BAD: {name:\"Exterior painting\",amount:4800} (painting as a flat price, so no crew hours); {name:\"Paint and materials\",amount:650} or {name:\"Duration (5 gal)\",amount:450} (separate materials line). Remove draft lines with removeLineItemIds (idempotent).",
      "x-ops-scopes": [
        "proposals-estimates"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_save_proposal_draft"
      },
      "x-examples": [
        {
          "summary": "Price painting through an area: substrates carry product + production rate + coats (hours and materials are computed)",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "save_proposal_draft",
            "input": {
              "estimateId": "ESTIMATE_ID",
              "areas": [
                {
                  "id": "living-room",
                  "name": "Living room",
                  "length": 18,
                  "width": 15,
                  "height": 9,
                  "substrates": [
                    {
                      "id": "living-room-walls",
                      "name": "Walls",
                      "productionRateId": "PRODUCTION_RATE_ID",
                      "productId": "PRODUCT_ID",
                      "coats": 2
                    },
                    {
                      "id": "living-room-doors",
                      "name": "Doors",
                      "productionRateId": "PRODUCTION_RATE_ID",
                      "productId": "PRODUCT_ID",
                      "coats": 2,
                      "qty": 2
                    }
                  ]
                }
              ]
            }
          }
        },
        {
          "summary": "Flat add-on (lift rental): priced lineItem with flatAddOn:true",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "save_proposal_draft",
            "input": {
              "estimateId": "ESTIMATE_ID",
              "lineItems": [
                {
                  "id": "lift-rental",
                  "name": "Boom lift rental",
                  "description": "One-week 45 ft boom lift rental, delivery and pickup included.",
                  "amount": 1200,
                  "flatAddOn": true
                }
              ]
            }
          }
        },
        {
          "summary": "Exclusions as one line per bullet (not in descriptor description)",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "save_proposal_draft",
            "input": {
              "estimateId": "ESTIMATE_ID",
              "exclusions": [
                "Soffits and fascia are excluded.",
                "Moisture-source repairs behind siding are excluded."
              ],
              "customerSummary": "Prep and paint main body per scope.",
              "descriptorIds": [
                "ps_desc_interior-preparation",
                "ps_desc_wall-painting"
              ]
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "Price painting through an area: substrates carry product + production rate + coats (hours and materials are computed)",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "save_proposal_draft",
          "input": {
            "estimateId": "ESTIMATE_ID",
            "areas": [
              {
                "id": "living-room",
                "name": "Living room",
                "length": 18,
                "width": 15,
                "height": 9,
                "substrates": [
                  {
                    "id": "living-room-walls",
                    "name": "Walls",
                    "productionRateId": "PRODUCTION_RATE_ID",
                    "productId": "PRODUCT_ID",
                    "coats": 2
                  },
                  {
                    "id": "living-room-doors",
                    "name": "Doors",
                    "productionRateId": "PRODUCTION_RATE_ID",
                    "productId": "PRODUCT_ID",
                    "coats": 2,
                    "qty": 2
                  }
                ]
              }
            ]
          }
        }
      }
    },
    {
      "name": "scan_shifts",
      "kind": "read",
      "description": "Scan the time-clock for anomalies — duplicate punches, overlapping shifts, or abnormal-length shifts (very long, very short, missing clock-out). Pure deterministic detector, no AI needed for the scan itself. Returns a grouped summary with counts and a sample. Use when admin asks 'are there any time clock issues?', 'find shift problems', 'scan the timeclock', or before running payroll.",
      "x-ops-scopes": [
        "briefing-read",
        "scheduling"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_scan_shifts"
      }
    },
    {
      "name": "score_lead",
      "kind": "read",
      "description": "Lead/admin sales tool. Score one lead/job or rank the top leads to call today using recency, source quality, estimate value, and idle days. Uses the authenticated active org only; no orgId is accepted.",
      "x-ops-scopes": [
        "leads-crm"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_score_lead"
      }
    },
    {
      "name": "send_proposal",
      "kind": "write",
      "description": "Send the draft proposal to the job's customer (email + share link). Same effect as Estimate Builder Send — freezes presentation and stamps proposalSent. Requires estimateId or jobId. Does NOT accept a custom recipient email (uses estimate/job customerEmail only). Does NOT share/publish to arbitrary addresses. Idempotent if already sent. If the notify state is unknown, do not send again. After staff confirm the customer was or was not notified, close that claim with a new Idempotency-Key and resolveUnknownNotify \"delivered\" (no second email or text). No orgId.",
      "x-ops-scopes": [
        "proposals-estimates"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_send_proposal"
      },
      "x-examples": [
        {
          "summary": "send_proposal — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "send_proposal",
            "input": {
              "estimateId": "ESTIMATE_ID",
              "jobId": "JOB_ID",
              "reason": "example",
              "resolveUnknownNotify": "delivered"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "send_proposal — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "send_proposal",
          "input": {
            "estimateId": "ESTIMATE_ID",
            "jobId": "JOB_ID",
            "reason": "example",
            "resolveUnknownNotify": "delivered"
          }
        }
      }
    },
    {
      "name": "set_home_file_access",
      "kind": "write",
      "description": "Portal Ops API. Save staff-only access info on a customer's Home File: gate code, lockbox, access notes and pets. Stored on the contact, never shown or sent to the customer. Only the fields you send change; send an empty string to clear one. For one customer pass contactId plus fields; for several pass items[] (up to 20) with the same shape. Get contactId from read_home_file. No prices. NEVER pass orgId.",
      "x-ops-scopes": [
        "home-file"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_set_home_file_access"
      },
      "x-examples": [
        {
          "summary": "set_home_file_access — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "set_home_file_access",
            "input": {
              "contactId": "example",
              "gateCode": "example",
              "lockbox": "example",
              "accessNotes": "example",
              "pets": "example",
              "items": []
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "set_home_file_access — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "set_home_file_access",
          "input": {
            "contactId": "example",
            "gateCode": "example",
            "lockbox": "example",
            "accessNotes": "example",
            "pets": "example",
            "items": []
          }
        }
      }
    },
    {
      "name": "set_home_file_formulas",
      "kind": "write",
      "description": "Portal Ops API. Save exact paint formulas on a job's Home File: Sherwin-Williams color number, sheen, product line and custom-match formula text, per color (and optionally per area). Rows with an id replace that row; rows without one are added; removeIds deletes rows; replace:true swaps the whole list. For one job pass jobId plus formulas[]; for several pass items[] (up to 20). Staff-only data; nothing is sent to the customer. NO prices. NEVER pass orgId.",
      "x-ops-scopes": [
        "home-file"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_set_home_file_formulas"
      },
      "x-examples": [
        {
          "summary": "set_home_file_formulas — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "set_home_file_formulas",
            "input": {
              "jobId": "JOB_ID",
              "formulas": [],
              "removeIds": [],
              "replace": false,
              "items": []
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "set_home_file_formulas — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "set_home_file_formulas",
          "input": {
            "jobId": "JOB_ID",
            "formulas": [],
            "removeIds": [],
            "replace": false,
            "items": []
          }
        }
      }
    },
    {
      "name": "set_home_file_leftovers",
      "kind": "write",
      "description": "Portal Ops API. Log leftover paint on a job's Home File: which cans were left at the house (color, product, sheen, amount) and where they are stored. Rows with an id replace that row; rows without one are added; removeIds deletes rows; replace:true swaps the whole list. For one job pass jobId plus leftovers[]; for several pass items[] (up to 20). Staff-only data; nothing is sent to the customer. NO prices. NEVER pass orgId.",
      "x-ops-scopes": [
        "home-file"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_set_home_file_leftovers"
      },
      "x-examples": [
        {
          "summary": "set_home_file_leftovers — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "set_home_file_leftovers",
            "input": {
              "jobId": "JOB_ID",
              "leftovers": [],
              "removeIds": [],
              "replace": false,
              "items": []
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "set_home_file_leftovers — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "set_home_file_leftovers",
          "input": {
            "jobId": "JOB_ID",
            "leftovers": [],
            "removeIds": [],
            "replace": false,
            "items": []
          }
        }
      }
    },
    {
      "name": "set_job_colors",
      "kind": "write",
      "description": "Set customer paint colors and short prep notes on a job (job.colorPlan). Rooms come from the estimate, one row per room. Set one room's color, mark sameAsNow, fillAll, or fillRooms with fillColor. Also pets and colorConsultRequested. Do not pass access, gate codes, or lockbox codes. Unconfirmed note suggestions are not saved colors until this tool writes them. NEVER pass orgId. No dollar amounts.",
      "x-ops-scopes": [
        "jobs-files"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_set_job_colors"
      },
      "x-examples": [
        {
          "summary": "set_job_colors — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "set_job_colors",
            "input": {
              "jobId": "JOB_ID",
              "room": "example",
              "color": "example",
              "sameAsNow": false,
              "fillAll": "example",
              "fillColor": "example",
              "fillRooms": [],
              "rooms": [],
              "pets": "example",
              "colorConsultRequested": false
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "set_job_colors — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "set_job_colors",
          "input": {
            "jobId": "JOB_ID",
            "room": "example",
            "color": "example",
            "sameAsNow": false,
            "fillAll": "example",
            "fillColor": "example",
            "fillRooms": [],
            "rooms": [],
            "pets": "example",
            "colorConsultRequested": false
          }
        }
      }
    },
    {
      "name": "set_job_name",
      "kind": "write",
      "description": "Alias of rename_job. Rename an existing job's title to the PaintScout PO/quote number (e.g. Kim Brandt → name '1253'). Optional contact fields only (customerName, phone, email, address). Does NOT change status, hours, billing, assignees, or send anything. Prefer this over edit_job when fixing a wrong customer-name title. Alias: set_job_name.",
      "x-ops-scopes": [
        "jobs-files"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_set_job_name"
      },
      "x-examples": [
        {
          "summary": "set_job_name — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "set_job_name",
            "input": {
              "jobId": "JOB_ID",
              "name": "example",
              "jobNumber": "example",
              "poNumber": "example",
              "quoteNumber": "example",
              "customerName": "example",
              "address": "example",
              "phone": "5735550100",
              "customerPhone": "5735550100",
              "email": "example",
              "customerEmail": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "set_job_name — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "set_job_name",
          "input": {
            "jobId": "JOB_ID",
            "name": "example",
            "jobNumber": "example",
            "poNumber": "example",
            "quoteNumber": "example",
            "customerName": "example",
            "address": "example",
            "phone": "5735550100",
            "customerPhone": "5735550100",
            "email": "example",
            "customerEmail": "example"
          }
        }
      }
    },
    {
      "name": "set_job_status",
      "kind": "write",
      "description": "Change job status only (same options as the job status UI): estimate, active, accepted, limbo (with limboReason staff note), completed, archived/complete, generalJob, billingType. For limbo, pass limboReason directly or call polish_limbo_reason first (Rewrite with AI parity). Prefer this over edit_job when only status/limbo changes. No orgId.",
      "x-ops-scopes": [
        "job-status"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_set_job_status"
      },
      "x-examples": [
        {
          "summary": "set_job_status — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "set_job_status",
            "input": {
              "jobId": "JOB_ID",
              "status": "estimate",
              "limboReason": "example",
              "generalJob": false,
              "billingType": "fixed"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "set_job_status — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "set_job_status",
          "input": {
            "jobId": "JOB_ID",
            "status": "estimate",
            "limboReason": "example",
            "generalJob": false,
            "billingType": "fixed"
          }
        }
      }
    },
    {
      "name": "suggest_burden_from_wages",
      "kind": "read",
      "description": "Admin-only. Convert crew base wages into a suggested fully-loaded burden $/hr (default load factor 1.35 for taxes/WC/benefits). Use after the admin tells you what ASA, Seth, or others make. Then call save_numbers_settings with the suggested burden (and wageSamples) so they can Confirm. Never invent wages. No orgId.",
      "x-ops-scopes": [
        "owner-finance"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_suggest_burden_from_wages"
      }
    },
    {
      "name": "toggle_recurring_reminder",
      "kind": "write",
      "description": "Pause or resume a recurring reminder without deleting it. Use when admin says 'pause the Monday reminder for now', 'turn off the walkthrough reminder until we're back', 're-enable the clock-out nudge'. Call list_recurring_reminders first to get the id.",
      "x-ops-scopes": [
        "company-state"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_toggle_recurring_reminder"
      },
      "x-examples": [
        {
          "summary": "toggle_recurring_reminder — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "toggle_recurring_reminder",
            "input": {
              "id": "example",
              "enabled": false
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "toggle_recurring_reminder — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "toggle_recurring_reminder",
          "input": {
            "id": "example",
            "enabled": false
          }
        }
      }
    },
    {
      "name": "undo_app_import",
      "kind": "write",
      "description": "Admin-only MUTATING. Undo one import batch: records the batch created are deleted, records it updated get back only the fields the import changed. Hours logged after the import and any later edit stay. A job with logged hours is not deleted and is not set back to estimate; those jobs are listed in conflicts. Safe to repeat (alreadyUndone). Sends nothing to customers. NEVER pass orgId.",
      "x-ops-scopes": [
        "app-import"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_undo_app_import"
      },
      "x-examples": [
        {
          "summary": "undo_app_import — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "undo_app_import",
            "input": {
              "importBatchId": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "undo_app_import — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "undo_app_import",
          "input": {
            "importBatchId": "example"
          }
        }
      }
    },
    {
      "name": "unlink_recording",
      "kind": "write",
      "description": "Remove one linked recording from a job by source+recordingId. Requires proposals-estimates or jobs-files. Pass dryRun:true to preview. No orgId.",
      "x-ops-scopes": [
        "proposals-estimates",
        "jobs-files"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_unlink_recording"
      },
      "x-examples": [
        {
          "summary": "unlink_recording — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "unlink_recording",
            "input": {
              "estimateId": "ESTIMATE_ID",
              "jobRef": "example",
              "jobId": "JOB_ID",
              "source": "plaud",
              "recordingId": "example",
              "dryRun": false
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "unlink_recording — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "unlink_recording",
          "input": {
            "estimateId": "ESTIMATE_ID",
            "jobRef": "example",
            "jobId": "JOB_ID",
            "source": "plaud",
            "recordingId": "example",
            "dryRun": false
          }
        }
      }
    },
    {
      "name": "unsend_proposal_to_draft",
      "kind": "write",
      "description": "Alias of reopen_proposal_draft. Reopen a SENT proposal to draft so save_proposal_draft can edit it (scope cut). Clears estimate status/sentAt, frozen presentation, and job proposalSent mirrors. Does NOT send/share/publish. Does NOT reopen accepted/signed (use change orders). Idempotent if already an editable draft. Pass estimateId or jobId. Aliases: reopen_estimate_draft, unsend_proposal_to_draft. No orgId.",
      "x-ops-scopes": [
        "proposals-estimates"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_unsend_proposal_to_draft"
      },
      "x-examples": [
        {
          "summary": "unsend_proposal_to_draft — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "unsend_proposal_to_draft",
            "input": {
              "estimateId": "ESTIMATE_ID",
              "jobId": "JOB_ID"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "unsend_proposal_to_draft — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "unsend_proposal_to_draft",
          "input": {
            "estimateId": "ESTIMATE_ID",
            "jobId": "JOB_ID"
          }
        }
      }
    },
    {
      "name": "update_company_info",
      "kind": "write",
      "description": "Update a company-wide setting stored in settings/main. Allowed fields: businessName, phone, email, paintStoreName, defaultHourlyRate.",
      "x-ops-scopes": [
        "company-state"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_update_company_info"
      },
      "x-examples": [
        {
          "summary": "update_company_info — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "update_company_info",
            "input": {
              "field": "businessName",
              "value": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "update_company_info — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "update_company_info",
          "input": {
            "field": "businessName",
            "value": "example"
          }
        }
      }
    },
    {
      "name": "update_drip_sequence",
      "kind": "write",
      "description": "Admin-only. Update an existing drip sequence (name, enabled, trigger, cancelOnEvents, steps). Confirm required. Call list_drip_sequences first for ids. No orgId.",
      "x-ops-scopes": [
        "drips-comms"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_update_drip_sequence"
      },
      "x-examples": [
        {
          "summary": "update_drip_sequence — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "update_drip_sequence",
            "input": {
              "sequenceId": "example",
              "name": "example",
              "enabled": false,
              "trigger": {},
              "cancelOnEvents": [],
              "steps": []
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "update_drip_sequence — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "update_drip_sequence",
          "input": {
            "sequenceId": "example",
            "name": "example",
            "enabled": false,
            "trigger": {},
            "cancelOnEvents": [],
            "steps": []
          }
        }
      }
    },
    {
      "name": "update_email_template",
      "kind": "write",
      "description": "Admin-only. Save one alert email/SMS template to Branding for the active org. Confirm card required. Pass kind + subject/heading/body/ctaLabel/sms. Prefer draft_email_template first. No orgId.",
      "x-ops-scopes": [
        "drips-comms"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_update_email_template"
      },
      "x-examples": [
        {
          "summary": "update_email_template — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "update_email_template",
            "input": {
              "kind": "example",
              "subject": "example",
              "heading": "example",
              "body": "example",
              "ctaLabel": "example",
              "sms": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "update_email_template — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "update_email_template",
          "input": {
            "kind": "example",
            "subject": "example",
            "heading": "example",
            "body": "example",
            "ctaLabel": "example",
            "sms": "example"
          }
        }
      }
    },
    {
      "name": "update_estimate",
      "kind": "write",
      "description": "PREFERRED voice estimate edit: one upsert/merge call for rooms (L×W×H), substrates, descriptors/colors, notes, exclusions, and pricing on an existing draft. Pass natural jobRef (PO/quote job.name, customer, address, or jobId) — server resolves; ambiguous refs return candidates (never guess). Empty arrays are no-ops; remove rooms via removeAreaIds; detach descriptors via removeDescriptorIds; remove colors via removeColorIds; remove lineItems via removeLineItemIds. PRICING RULE: price painting and staining through areas[] (substrates with productId, productionRateId, coats) so the crew work order gets hours and materials. Use a flat-priced lineItem only for an add-on such as lift or equipment rental (set flatAddOn:true). Never add a separate paint/stain materials line; materials come from the products on area substrates. GOOD: areas:[{name:\"Living room\",length:18,width:15,height:9,substrates:[{name:\"Walls\",productionRateId,productId,coats:2}]}] plus lineItems:[{name:\"Boom lift rental\",amount:1200,flatAddOn:true}]. BAD: {name:\"Exterior painting\",amount:4800} (painting as a flat price, so no crew hours); {name:\"Paint and materials\",amount:650} or {name:\"Duration (5 gal)\",amount:450} (separate materials line). Returns compact estimateSnapshot + recomputed totals (same engine as Estimate Builder) — no verification read needed. Does NOT send or create jobs. No orgId.",
      "x-ops-scopes": [
        "proposals-estimates"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_update_estimate"
      },
      "x-examples": [
        {
          "summary": "update_estimate — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "update_estimate",
            "input": {
              "jobRef": "example",
              "jobId": "JOB_ID",
              "estimateId": "ESTIMATE_ID",
              "removeAreaIds": [],
              "removeColorIds": [],
              "removeDescriptorIds": [],
              "removeLineItemIds": [],
              "patchArea": [
                "First exclusion line.",
                "Second exclusion line."
              ],
              "patchLineItem": [
                "First exclusion line.",
                "Second exclusion line."
              ],
              "areas": [],
              "lineItems": [],
              "descriptorIds": [],
              "selectedDescriptors": [],
              "descriptors": [],
              "addDescriptorIds": [],
              "colors": [],
              "exclusions": "example",
              "notes": "example",
              "customerSummary": "example",
              "crewNote": "example",
              "hourlyRate": 1,
              "taxRate": 1,
              "discount": "example",
              "wastePercent": 1,
              "hoursEstimate": 1
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "update_estimate — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "update_estimate",
          "input": {
            "jobRef": "example",
            "jobId": "JOB_ID",
            "estimateId": "ESTIMATE_ID",
            "removeAreaIds": [],
            "removeColorIds": [],
            "removeDescriptorIds": [],
            "removeLineItemIds": [],
            "patchArea": [
              "First exclusion line.",
              "Second exclusion line."
            ],
            "patchLineItem": [
              "First exclusion line.",
              "Second exclusion line."
            ],
            "areas": [],
            "lineItems": [],
            "descriptorIds": [],
            "selectedDescriptors": [],
            "descriptors": [],
            "addDescriptorIds": [],
            "colors": [],
            "exclusions": "example",
            "notes": "example",
            "customerSummary": "example",
            "crewNote": "example",
            "hourlyRate": 1,
            "taxRate": 1,
            "discount": "example",
            "wastePercent": 1,
            "hoursEstimate": 1
          }
        }
      }
    },
    {
      "name": "upload_job_file",
      "kind": "write",
      "description": "Attach a PaintScout / work-order PDF to an existing job. Pass jobId + pdfBase64 (or fileBase64) + fileName. Stores under orgs/{orgId}/jobs/{jobId}/… and appends job.files[]. Alias: attach_work_order.",
      "x-ops-scopes": [
        "jobs-files"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_upload_job_file"
      },
      "x-examples": [
        {
          "summary": "upload_job_file — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "upload_job_file",
            "input": {
              "jobId": "JOB_ID",
              "pdfBase64": "example",
              "fileBase64": "example",
              "fileName": "example",
              "kind": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "upload_job_file — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "upload_job_file",
          "input": {
            "jobId": "JOB_ID",
            "pdfBase64": "example",
            "fileBase64": "example",
            "fileName": "example",
            "kind": "example"
          }
        }
      }
    },
    {
      "name": "upload_photo",
      "kind": "write",
      "description": "Upload a photo to an estimate or job (base64 JPEG/PNG/WebP/HEIC, max 12 MB). Optional room/area, caption, takenAt. Lands staff-only (approval pending) — customers see it only after a staff member approves it in Portal. No orgId.",
      "x-ops-scopes": [
        "photos"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_upload_photo"
      },
      "x-examples": [
        {
          "summary": "upload_photo — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "upload_photo",
            "input": {
              "scope": "job",
              "jobId": "JOB_ID",
              "estimateId": "ESTIMATE_ID",
              "areaId": "example",
              "substrateId": "example",
              "imageBase64": "example",
              "contentType": "example",
              "fileName": "example",
              "room": "example",
              "caption": "example",
              "takenAt": 1
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "upload_photo — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "upload_photo",
          "input": {
            "scope": "job",
            "jobId": "JOB_ID",
            "estimateId": "ESTIMATE_ID",
            "areaId": "example",
            "substrateId": "example",
            "imageBase64": "example",
            "contentType": "example",
            "fileName": "example",
            "room": "example",
            "caption": "example",
            "takenAt": 1
          }
        }
      }
    },
    {
      "name": "upsert_estimate_descriptor",
      "kind": "write",
      "description": "Admin-only MUTATING. Add or update a library descriptor (title + description proposal block). Confirm card is the review step. For edits pass id from list_estimate_descriptors. This writes the org-wide LIBRARY — not a single estimate's lineItems. Do NOT use it to add or price a line on one estimate (use save_proposal_draft / update_estimate): a call that names an estimate, proposal, job or customer (estimateId, estimateRef, estimate_id, proposalId, jobId, jobRef, job, customer, …) is rejected with descriptor-scope-required and writes nothing — scope:\"library\" does not override it; resend without those keys to edit the library. Library fields are stored, including price and flatAddOn for an add-on such as a lift rental. Area pricing goes through hours and materials. Hours, dimensions, substrates, rate/product ids, and estimate/job keys are refused.",
      "x-ops-scopes": [
        "proposals-estimates"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_upsert_estimate_descriptor"
      },
      "x-examples": [
        {
          "summary": "upsert_estimate_descriptor — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "upsert_estimate_descriptor",
            "input": {
              "scope": "library",
              "id": "example",
              "name": "example",
              "description": "example",
              "crewNote": "example",
              "clientNote": "example",
              "materials": "example",
              "estimateTypes": [],
              "sort": 1,
              "price": 1,
              "flatAddOn": false
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "upsert_estimate_descriptor — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "upsert_estimate_descriptor",
          "input": {
            "scope": "library",
            "id": "example",
            "name": "example",
            "description": "example",
            "crewNote": "example",
            "clientNote": "example",
            "materials": "example",
            "estimateTypes": [],
            "sort": 1,
            "price": 1,
            "flatAddOn": false
          }
        }
      }
    },
    {
      "name": "upsert_estimate_product",
      "kind": "write",
      "description": "Admin-only MUTATING. Add or update a product in the org product library (orgs/.../estimateProducts). Confirm card is the review step — only call AFTER the admin said yes to saving to the product library/index. For edits pass id from list_estimate_products. On create: name + pricePerUnit ($/gal) + coveragePerUnit (sqft/gal) required. Optional pds/productKey from research_estimate_product. Never invent price or coverage.",
      "x-ops-scopes": [
        "proposals-estimates"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_upsert_estimate_product"
      },
      "x-examples": [
        {
          "summary": "upsert_estimate_product — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "upsert_estimate_product",
            "input": {
              "id": "example",
              "name": "example",
              "group": "example",
              "finish": "example",
              "coveragePerUnit": 1,
              "coverageLnft": 1,
              "galPerItem": 1,
              "pricePerUnit": 1,
              "unit": "example",
              "swatch": "example",
              "productKey": "example",
              "pds": {},
              "estimateTypes": [],
              "sort": 1
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "upsert_estimate_product — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "upsert_estimate_product",
          "input": {
            "id": "example",
            "name": "example",
            "group": "example",
            "finish": "example",
            "coveragePerUnit": 1,
            "coverageLnft": 1,
            "galPerItem": 1,
            "pricePerUnit": 1,
            "unit": "example",
            "swatch": "example",
            "productKey": "example",
            "pds": {},
            "estimateTypes": [],
            "sort": 1
          }
        }
      }
    },
    {
      "name": "upsert_production_rate",
      "kind": "write",
      "description": "Admin-only MUTATING. Add or update a library production rate (orgs/.../productionRates). Confirm card is the review step. For edits, pass id from list_production_rates — never invent ids. For adds, omit id (client slugs one). ratesByCoats is required on create (e.g. {\"1\":120,\"2\":85}).",
      "x-ops-scopes": [
        "production-rates"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_upsert_production_rate"
      },
      "x-examples": [
        {
          "summary": "upsert_production_rate — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "upsert_production_rate",
            "input": {
              "id": "example",
              "name": "example",
              "group": "example",
              "type": "sqft_hr",
              "measureMode": "walls",
              "unitLabel": "example",
              "ratesByCoats": {},
              "clientLabel": "example",
              "defaultCoats": 1,
              "defaultProductId": "example",
              "prepHours": 1,
              "defaultQty": 1,
              "crewNote": "example",
              "areaDescription": "example",
              "substrateDescription": "example",
              "estimateTypes": [],
              "sort": 1,
              "standard": false
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "upsert_production_rate — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "upsert_production_rate",
          "input": {
            "id": "example",
            "name": "example",
            "group": "example",
            "type": "sqft_hr",
            "measureMode": "walls",
            "unitLabel": "example",
            "ratesByCoats": {},
            "clientLabel": "example",
            "defaultCoats": 1,
            "defaultProductId": "example",
            "prepHours": 1,
            "defaultQty": 1,
            "crewNote": "example",
            "areaDescription": "example",
            "substrateDescription": "example",
            "estimateTypes": [],
            "sort": 1,
            "standard": false
          }
        }
      }
    },
    {
      "name": "void_receipt",
      "kind": "write",
      "description": "Portal Ops only (Owner $ scope). MUTATING. Undo a confirmed receipt that was posted to the wrong job. The expense ledger is append-only, so this adds ONE negative reversal entry (the original line stays), takes the file off that job's costing, and returns the receipt to needs_review for a person to re-file in Numbers — it is never auto-confirmed again. Safe to repeat. NEVER pass orgId.",
      "x-ops-scopes": [
        "owner-finance"
      ],
      "inputSchema": {
        "$ref": "#/components/schemas/PortalOpsInput_void_receipt"
      },
      "x-examples": [
        {
          "summary": "void_receipt — minimal exec payload",
          "value": {
            "action": "exec",
            "orgId": "your-org-id",
            "toolName": "void_receipt",
            "input": {
              "id": "example",
              "reason": "example"
            }
          }
        }
      ],
      "x-portal-ops-example": {
        "summary": "void_receipt — minimal exec payload",
        "value": {
          "action": "exec",
          "orgId": "your-org-id",
          "toolName": "void_receipt",
          "input": {
            "id": "example",
            "reason": "example"
          }
        }
      }
    }
  ],
  "x-portal-ops-blocked-tools": [
    {
      "name": "share_proposal",
      "kind": "blocked",
      "description": "Not available on Portal Ops keys (send-blocked).",
      "x-ops-block-code": "send-blocked"
    },
    {
      "name": "publish_proposal",
      "kind": "blocked",
      "description": "Not available on Portal Ops keys (send-blocked).",
      "x-ops-block-code": "send-blocked"
    },
    {
      "name": "share_estimate",
      "kind": "blocked",
      "description": "Not available on Portal Ops keys (send-blocked).",
      "x-ops-block-code": "send-blocked"
    },
    {
      "name": "publish_estimate",
      "kind": "blocked",
      "description": "Not available on Portal Ops keys (send-blocked).",
      "x-ops-block-code": "send-blocked"
    },
    {
      "name": "persist_takeoff",
      "kind": "blocked",
      "description": "Not available on Portal Ops keys (create-estimate-blocked).",
      "x-ops-block-code": "create-estimate-blocked"
    },
    {
      "name": "save_takeoff",
      "kind": "blocked",
      "description": "Not available on Portal Ops keys (create-estimate-blocked).",
      "x-ops-block-code": "create-estimate-blocked"
    },
    {
      "name": "upload_vendor_invoice",
      "kind": "blocked",
      "description": "Not available on Portal Ops keys (vendor-invoice-key-required).",
      "x-ops-block-code": "vendor-invoice-key-required"
    },
    {
      "name": "upload_bank_statement",
      "kind": "blocked",
      "description": "Not available on Portal Ops keys (bank-statement-key-required).",
      "x-ops-block-code": "bank-statement-key-required"
    },
    {
      "name": "confirm_vendor_invoice",
      "kind": "blocked",
      "description": "Not available on Portal Ops keys (confirm-cost-blocked).",
      "x-ops-block-code": "confirm-cost-blocked"
    },
    {
      "name": "confirm_cost",
      "kind": "blocked",
      "description": "Not available on Portal Ops keys (confirm-cost-blocked).",
      "x-ops-block-code": "confirm-cost-blocked"
    }
  ],
  "x-portal-ops-http-notes": {
    "endpoint": "/.netlify/functions/portal-ops-mrz-tool",
    "deprecatedEndpoint": "/.netlify/functions/openclaw-mrz-tool",
    "deprecatedNote": "Thin re-export of portal-ops-mrz-tool; migrate clients off the legacy path.",
    "methods": [
      "POST",
      "OPTIONS"
    ],
    "rateLimit": {
      "limit": 60,
      "windowMs": 60000,
      "responseCode": 429,
      "body": {
        "error": "rate-limited",
        "retryMs": "number"
      }
    },
    "orgIdRules": "Top-level orgId must match the org embedded in the ppops key. Do not pass orgId inside tool input (stripped server-side).",
    "authHeaders": [
      "Authorization: Bearer <ppops key>",
      "X-Pezley-Ops-Key: <ppops key>"
    ],
    "actions": [
      "brief",
      "exec",
      "batch",
      "company-state",
      "log"
    ]
  },
  "paths": {
    "/.netlify/functions/portal-ops-mrz-tool": {
      "post": {
        "tags": [
          "portal-ops"
        ],
        "summary": "Portal Ops API",
        "description": "Portal Ops HTTP API. Actions: `brief` (morning snapshot), `company-state` (compact memory for follow-up turns), `exec` (one registry tool), `batch` (ordered ops[], optional atomic), `log` (audit session line). Ops keys: 60 requests/minute per uid (`429` + `retryMs`). `orgId` is required on the JSON body and must match the key's org. Authenticate with `Authorization: Bearer <ppops_…>` or `X-Pezley-Ops-Key`. Firebase admin Bearer tokens receive the full catalog on this URL.",
        "security": [
          {
            "OpsBriefBearer": []
          },
          {
            "OpsBriefHeader": []
          },
          {
            "FirebaseAdminBearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PortalOpsApiRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PortalOpsApiSuccess"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/PortalOpsBadRequest"
          },
          "401": {
            "$ref": "#/components/responses/PortalOpsUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/PortalOpsForbidden"
          },
          "429": {
            "$ref": "#/components/responses/PortalOpsRateLimited"
          }
        }
      }
    },
    "/.netlify/functions/openclaw-mrz-tool": {
      "post": {
        "deprecated": true,
        "tags": [
          "portal-ops"
        ],
        "summary": "Portal Ops API (deprecated alias)",
        "description": "**Deprecated URL** — thin re-export of `portal-ops-mrz-tool`. Use `POST /.netlify/functions/portal-ops-mrz-tool` for new integrations. Request and response bodies are identical. Portal Ops HTTP API. Actions: `brief` (morning snapshot), `company-state` (compact memory for follow-up turns), `exec` (one registry tool), `batch` (ordered ops[], optional atomic), `log` (audit session line). Ops keys: 60 requests/minute per uid (`429` + `retryMs`). `orgId` is required on the JSON body and must match the key's org. Authenticate with `Authorization: Bearer <ppops_…>` or `X-Pezley-Ops-Key`. Firebase admin Bearer tokens receive the full catalog on this URL.",
        "security": [
          {
            "OpsBriefBearer": []
          },
          {
            "OpsBriefHeader": []
          },
          {
            "FirebaseAdminBearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PortalOpsApiRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PortalOpsApiSuccess"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/PortalOpsBadRequest"
          },
          "401": {
            "$ref": "#/components/responses/PortalOpsUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/PortalOpsForbidden"
          },
          "429": {
            "$ref": "#/components/responses/PortalOpsRateLimited"
          }
        }
      }
    },
    "/.netlify/functions/ops-brief-key": {
      "post": {
        "tags": [
          "portal-ops-keys"
        ],
        "summary": "Manage Portal Ops keys (admin UI parity)",
        "description": "Org admins only. Requires `Authorization: Bearer <Firebase ID token>` for a user with org admin role — not a `ppops_` key.",
        "security": [
          {
            "FirebaseAdminBearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OpsBriefKeyRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OpsBriefKeySuccess"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/PortalOpsBadRequest"
          },
          "401": {
            "$ref": "#/components/responses/PortalOpsUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/PortalOpsForbidden"
          },
          "404": {
            "$ref": "#/components/responses/PortalOpsNotFound"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "OpsBriefBearer": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "ppops_<orgId>_<64 hex>",
        "description": "Portal Ops key. Store as PEZLEY_OPS_BRIEF_KEY in the assistant environment."
      },
      "OpsBriefHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Pezley-Ops-Key",
        "description": "Same value as Bearer — alternate header for clients that prefer a named header."
      },
      "FirebaseAdminBearer": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "Firebase ID token",
        "description": "Signed-in org admin. Required for ops-brief-key management; optional on the Portal Ops API endpoint for the full admin tool catalog."
      }
    },
    "responses": {
      "PortalOpsUnauthorized": {
        "description": "Missing or invalid authentication",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/PortalOpsError"
            },
            "examples": {
              "missingAuth": {
                "value": {
                  "error": "missing-auth",
                  "code": "missing-auth"
                }
              },
              "invalidKey": {
                "value": {
                  "error": "invalid-ops-key",
                  "code": "invalid-ops-key"
                }
              }
            }
          }
        }
      },
      "PortalOpsForbidden": {
        "description": "Authenticated but not allowed for this key, scope, or org",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/PortalOpsError"
            },
            "examples": {
              "briefingScope": {
                "value": {
                  "error": "briefing-scope-required",
                  "code": "briefing-scope-required",
                  "hint": "Enable the briefing-read scope on this key."
                }
              },
              "financeScope": {
                "value": {
                  "error": "finance-scope-required",
                  "code": "finance-scope-required",
                  "toolName": "analyze_numbers"
                }
              },
              "scopeDenied": {
                "value": {
                  "error": "ops-brief-scope-denied",
                  "code": "ops-brief-scope-denied",
                  "toolName": "create_job"
                }
              },
              "sendBlocked": {
                "value": {
                  "error": "send-blocked",
                  "code": "send-blocked",
                  "toolName": "share_proposal"
                }
              }
            }
          }
        }
      },
      "PortalOpsBadRequest": {
        "description": "Invalid JSON or request shape",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/PortalOpsError"
            }
          }
        }
      },
      "PortalOpsNotFound": {
        "description": "Resource not found",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/PortalOpsError"
            }
          }
        }
      },
      "PortalOpsRateLimited": {
        "description": "Too many requests",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "error": {
                  "type": "string",
                  "example": "rate-limited"
                },
                "retryMs": {
                  "type": "integer"
                }
              }
            }
          }
        }
      }
    },
    "schemas": {
      "PortalOpsError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "enum": [
              "missing-auth",
              "invalid-ops-key",
              "org-mismatch",
              "briefing-scope-required",
              "ops-brief-scope-denied",
              "finance-scope-required",
              "numbers-cap-required",
              "ops-mcp-consent-stale",
              "send-blocked",
              "create-estimate-blocked",
              "vendor-invoice-key-required",
              "bank-statement-key-required",
              "confirm-cost-blocked",
              "estimator-key-wrong-endpoint",
              "vendor-invoice-key-wrong-endpoint",
              "bank-statement-key-wrong-endpoint",
              "rate-limited",
              "unknown action",
              "toolName required",
              "key-id-required",
              "ops-key-not-found",
              "ops-key-limit",
              "unknown-scope",
              "scopes-empty",
              "scopes-must-be-array",
              "legacy-key-scopes-immutable",
              "org-required",
              "version-conflict",
              "unknown-field",
              "invalid-if-version",
              "invalid-field",
              "revision-id-required",
              "revision-not-found",
              "revision-empty",
              "import-input-required",
              "import-file-too-large",
              "import-unpacked-too-large",
              "import-too-many-files",
              "import-unreadable",
              "previewId-required",
              "preview-not-found",
              "preview-not-ready",
              "preview-expired",
              "preview-incomplete",
              "commit-in-progress",
              "importBatchId-required",
              "batch-not-found",
              "batch-undone",
              "commit-lost",
              "bad-source",
              "bad-mode",
              "coming-soon",
              "bad-plan",
              "bad-payload",
              "invalid-line-item-ids",
              "descriptor-scope-required"
            ]
          },
          "hint": {
            "type": "string"
          },
          "toolName": {
            "type": "string"
          },
          "allowed": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "PortalOpsApiRequest": {
        "oneOf": [
          {
            "$ref": "#/components/schemas/BriefActionRequest"
          },
          {
            "$ref": "#/components/schemas/CompanyStateActionRequest"
          },
          {
            "$ref": "#/components/schemas/ExecActionRequest"
          },
          {
            "$ref": "#/components/schemas/BatchActionRequest"
          },
          {
            "$ref": "#/components/schemas/LogActionRequest"
          }
        ]
      },
      "BriefActionRequest": {
        "type": "object",
        "required": [
          "action",
          "orgId"
        ],
        "properties": {
          "action": {
            "type": "string",
            "enum": [
              "brief"
            ]
          },
          "orgId": {
            "type": "string",
            "description": "Must match the org embedded in the ppops key."
          },
          "sessionId": {
            "type": "string",
            "maxLength": 80
          },
          "adminName": {
            "type": "string",
            "description": "Optional display name in audit log."
          }
        }
      },
      "CompanyStateActionRequest": {
        "type": "object",
        "required": [
          "action",
          "orgId"
        ],
        "properties": {
          "action": {
            "type": "string",
            "enum": [
              "company-state"
            ]
          },
          "orgId": {
            "type": "string"
          },
          "sessionId": {
            "type": "string",
            "maxLength": 80
          },
          "adminName": {
            "type": "string"
          }
        }
      },
      "LogActionRequest": {
        "type": "object",
        "required": [
          "action",
          "orgId"
        ],
        "properties": {
          "action": {
            "type": "string",
            "enum": [
              "log"
            ]
          },
          "orgId": {
            "type": "string"
          },
          "sessionId": {
            "type": "string"
          },
          "kind": {
            "type": "string"
          },
          "message": {
            "type": "string"
          },
          "replyPreview": {
            "type": "string"
          },
          "tools": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "error": {
            "type": "string"
          }
        }
      },
      "ExecActionRequest": {
        "type": "object",
        "required": [
          "action",
          "orgId",
          "toolName"
        ],
        "properties": {
          "action": {
            "type": "string",
            "enum": [
              "exec"
            ]
          },
          "orgId": {
            "type": "string"
          },
          "toolName": {
            "type": "string",
            "enum": [
              "add_crew_member",
              "add_estimate_note",
              "add_job",
              "add_reminder",
              "add_shift_manual",
              "add_spot_assignment",
              "add_task_to_job",
              "add_tool",
              "advise_cfo",
              "analyze_numbers",
              "analyze_pipeline",
              "analyze_rate_book",
              "analyze_time_trends",
              "annotate_photo",
              "assign_to_job",
              "attach_job_file",
              "attach_work_order",
              "book_client_visit",
              "book_estimate_visit",
              "cancel_client_visit",
              "cancel_estimate_visit",
              "cfo_setup_status",
              "check_in_tool",
              "check_out_tool",
              "clock_in_user",
              "clock_out_user",
              "commit_app_import",
              "confirm_receipt",
              "create_drip_sequence",
              "create_estimate",
              "create_job",
              "create_lead",
              "create_recurring_reminder",
              "create_webhook",
              "delete_estimate_descriptor",
              "delete_estimate_product",
              "delete_job_file",
              "delete_production_rate",
              "delete_recurring_reminder",
              "delete_shift",
              "delete_tool",
              "delete_webhook",
              "detach_job_file",
              "download_photo",
              "download_proposal_pdf",
              "draft_cfo_report",
              "draft_drip_sequence",
              "draft_email_template",
              "draft_followup",
              "edit_crew_member",
              "edit_job",
              "edit_job_schedule",
              "edit_recurring_reminder",
              "edit_shift",
              "edit_summary",
              "edit_tool",
              "estimate_context",
              "estimate_snapshot",
              "finish_bank_reconciliation",
              "forget",
              "get_contact",
              "get_estimate",
              "get_estimate_descriptor",
              "get_estimate_product",
              "get_invoice",
              "get_job_production",
              "get_lead",
              "get_production_rate",
              "get_settings",
              "ingest_receipt",
              "link_recording",
              "list_activity",
              "list_app_import_batches",
              "list_appointments",
              "list_bank_inbox",
              "list_cfo_packs",
              "list_close_checklist",
              "list_contacts",
              "list_crew",
              "list_drip_sequences",
              "list_email_templates",
              "list_estimate_descriptors",
              "list_estimate_products",
              "list_estimates",
              "list_followups",
              "list_hours",
              "list_invoices",
              "list_job_colors",
              "list_job_costs",
              "list_jobs",
              "list_leads",
              "list_learning_ai",
              "list_messages",
              "list_payments",
              "list_pending_checkins",
              "list_pending_share_photos",
              "list_photos",
              "list_production",
              "list_production_rates",
              "list_proposals",
              "list_receipts",
              "list_recordings",
              "list_recurring_reminders",
              "list_reviews_referrals",
              "list_sales_leads",
              "list_schedule",
              "list_staff",
              "list_teams",
              "list_tools",
              "list_webhooks",
              "mark_tool_fixed",
              "mark_tool_repair",
              "match_bank_line",
              "offer_estimate_visit",
              "polish_delay_reason",
              "polish_limbo_reason",
              "post_job_delay",
              "preview_app_import",
              "publish_checkins",
              "query_cfo_knowledge",
              "read_estimate_handoff",
              "read_home_file",
              "read_job",
              "read_proposal",
              "read_recording_transcript",
              "record_collected_payment",
              "remember",
              "remove_job_file",
              "remove_spot_assignment",
              "rename_job",
              "reopen_estimate_draft",
              "reopen_proposal_draft",
              "research_estimate_product",
              "resend_proposal_email",
              "restore_proposal_revision",
              "save_annotated_photo",
              "save_estimate_settings",
              "save_job_costing",
              "save_numbers_settings",
              "save_proposal_draft",
              "scan_shifts",
              "score_lead",
              "send_proposal",
              "set_home_file_access",
              "set_home_file_formulas",
              "set_home_file_leftovers",
              "set_job_colors",
              "set_job_name",
              "set_job_status",
              "suggest_burden_from_wages",
              "toggle_recurring_reminder",
              "undo_app_import",
              "unlink_recording",
              "unsend_proposal_to_draft",
              "update_company_info",
              "update_drip_sequence",
              "update_email_template",
              "update_estimate",
              "upload_job_file",
              "upload_photo",
              "upsert_estimate_descriptor",
              "upsert_estimate_product",
              "upsert_production_rate",
              "void_receipt"
            ],
            "description": "See x-portal-ops-tools for descriptions, read/write kind, and x-ops-scopes."
          },
          "toolUseId": {
            "type": "string",
            "description": "Optional idempotency / dedupe key for some writes."
          },
          "input": {
            "type": "object",
            "description": "Tool-specific payload. Schema per tool in components/schemas/PortalOpsInput_*.",
            "additionalProperties": true
          }
        }
      },
      "BatchActionRequest": {
        "type": "object",
        "description": "Ordered tool calls (max 20). Top-level fields match explainOpsRequestBody's batch envelope (public guide fast-path: action, orgId, atomic, ops).",
        "required": [
          "action",
          "orgId",
          "ops"
        ],
        "properties": {
          "action": {
            "type": "string",
            "enum": [
              "batch"
            ]
          },
          "orgId": {
            "type": "string",
            "description": "Must match the org embedded in the ppops key."
          },
          "atomic": {
            "type": "boolean",
            "description": "When true, estimate writes roll back to the pre-batch snapshot if any op fails."
          },
          "ops": {
            "type": "array",
            "minItems": 1,
            "maxItems": 20,
            "items": {
              "type": "object",
              "required": [
                "toolName"
              ],
              "properties": {
                "toolName": {
                  "type": "string",
                  "enum": [
                    "add_crew_member",
                    "add_estimate_note",
                    "add_job",
                    "add_reminder",
                    "add_shift_manual",
                    "add_spot_assignment",
                    "add_task_to_job",
                    "add_tool",
                    "advise_cfo",
                    "analyze_numbers",
                    "analyze_pipeline",
                    "analyze_rate_book",
                    "analyze_time_trends",
                    "annotate_photo",
                    "assign_to_job",
                    "attach_job_file",
                    "attach_work_order",
                    "book_client_visit",
                    "book_estimate_visit",
                    "cancel_client_visit",
                    "cancel_estimate_visit",
                    "cfo_setup_status",
                    "check_in_tool",
                    "check_out_tool",
                    "clock_in_user",
                    "clock_out_user",
                    "commit_app_import",
                    "confirm_receipt",
                    "create_drip_sequence",
                    "create_estimate",
                    "create_job",
                    "create_lead",
                    "create_recurring_reminder",
                    "create_webhook",
                    "delete_estimate_descriptor",
                    "delete_estimate_product",
                    "delete_job_file",
                    "delete_production_rate",
                    "delete_recurring_reminder",
                    "delete_shift",
                    "delete_tool",
                    "delete_webhook",
                    "detach_job_file",
                    "download_photo",
                    "download_proposal_pdf",
                    "draft_cfo_report",
                    "draft_drip_sequence",
                    "draft_email_template",
                    "draft_followup",
                    "edit_crew_member",
                    "edit_job",
                    "edit_job_schedule",
                    "edit_recurring_reminder",
                    "edit_shift",
                    "edit_summary",
                    "edit_tool",
                    "estimate_context",
                    "estimate_snapshot",
                    "finish_bank_reconciliation",
                    "forget",
                    "get_contact",
                    "get_estimate",
                    "get_estimate_descriptor",
                    "get_estimate_product",
                    "get_invoice",
                    "get_job_production",
                    "get_lead",
                    "get_production_rate",
                    "get_settings",
                    "ingest_receipt",
                    "link_recording",
                    "list_activity",
                    "list_app_import_batches",
                    "list_appointments",
                    "list_bank_inbox",
                    "list_cfo_packs",
                    "list_close_checklist",
                    "list_contacts",
                    "list_crew",
                    "list_drip_sequences",
                    "list_email_templates",
                    "list_estimate_descriptors",
                    "list_estimate_products",
                    "list_estimates",
                    "list_followups",
                    "list_hours",
                    "list_invoices",
                    "list_job_colors",
                    "list_job_costs",
                    "list_jobs",
                    "list_leads",
                    "list_learning_ai",
                    "list_messages",
                    "list_payments",
                    "list_pending_checkins",
                    "list_pending_share_photos",
                    "list_photos",
                    "list_production",
                    "list_production_rates",
                    "list_proposals",
                    "list_receipts",
                    "list_recordings",
                    "list_recurring_reminders",
                    "list_reviews_referrals",
                    "list_sales_leads",
                    "list_schedule",
                    "list_staff",
                    "list_teams",
                    "list_tools",
                    "list_webhooks",
                    "mark_tool_fixed",
                    "mark_tool_repair",
                    "match_bank_line",
                    "offer_estimate_visit",
                    "polish_delay_reason",
                    "polish_limbo_reason",
                    "post_job_delay",
                    "preview_app_import",
                    "publish_checkins",
                    "query_cfo_knowledge",
                    "read_estimate_handoff",
                    "read_home_file",
                    "read_job",
                    "read_proposal",
                    "read_recording_transcript",
                    "record_collected_payment",
                    "remember",
                    "remove_job_file",
                    "remove_spot_assignment",
                    "rename_job",
                    "reopen_estimate_draft",
                    "reopen_proposal_draft",
                    "research_estimate_product",
                    "resend_proposal_email",
                    "restore_proposal_revision",
                    "save_annotated_photo",
                    "save_estimate_settings",
                    "save_job_costing",
                    "save_numbers_settings",
                    "save_proposal_draft",
                    "scan_shifts",
                    "score_lead",
                    "send_proposal",
                    "set_home_file_access",
                    "set_home_file_formulas",
                    "set_home_file_leftovers",
                    "set_job_colors",
                    "set_job_name",
                    "set_job_status",
                    "suggest_burden_from_wages",
                    "toggle_recurring_reminder",
                    "undo_app_import",
                    "unlink_recording",
                    "unsend_proposal_to_draft",
                    "update_company_info",
                    "update_drip_sequence",
                    "update_email_template",
                    "update_estimate",
                    "upload_job_file",
                    "upload_photo",
                    "upsert_estimate_descriptor",
                    "upsert_estimate_product",
                    "upsert_production_rate",
                    "void_receipt"
                  ]
                },
                "input": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "sessionId": {
            "type": "string",
            "maxLength": 80
          },
          "adminName": {
            "type": "string"
          },
          "toolUseId": {
            "type": "string"
          },
          "idempotencyKey": {
            "type": "string",
            "description": "24h idempotent retry key (or the Idempotency-Key header)."
          },
          "requestId": {
            "type": "string"
          }
        }
      },
      "PortalOpsBriefSnapshot": {
        "type": "object",
        "description": "High-level morning brief payload (action brief). Requires briefing-read scope on ops keys.",
        "properties": {
          "asOf": {
            "type": "string",
            "format": "date-time"
          },
          "today": {
            "type": "string",
            "description": "YYYY-MM-DD in org timezone."
          },
          "orgId": {
            "type": "string"
          },
          "learningAi": {
            "type": "object"
          },
          "todayJobs": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "weekJobs": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "onClock": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "hours": {
            "type": "object",
            "description": "Pay-period rollups per crew member."
          },
          "salesLeads": {
            "type": "object",
            "description": "Hub To-build leads (list_sales_leads shape)."
          },
          "proposals": {
            "type": "object",
            "description": "Hub proposals list (list_proposals shape)."
          },
          "pipeline": {
            "type": "object"
          },
          "pendingCheckins": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "crew": {
            "type": "array",
            "items": {
              "type": "object"
            }
          }
        }
      },
      "PortalOpsApiSuccess": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "action": {
            "type": "string",
            "enum": [
              "brief",
              "exec",
              "batch",
              "company-state",
              "log"
            ]
          },
          "orgId": {
            "type": "string"
          },
          "brief": {
            "$ref": "#/components/schemas/PortalOpsBriefSnapshot"
          },
          "companyState": {
            "type": "object",
            "description": "Subset of brief + memory fields for admin-claude handoff."
          },
          "result": {
            "type": "object",
            "description": "Present when action is exec — tool-specific."
          }
        }
      },
      "OpsBriefKeyRequest": {
        "type": "object",
        "required": [
          "action",
          "orgId"
        ],
        "properties": {
          "action": {
            "type": "string",
            "enum": [
              "key-status",
              "save-mcp-connection",
              "generate-key",
              "update-key-scopes",
              "rotate-key",
              "revoke-key"
            ]
          },
          "orgId": {
            "type": "string"
          },
          "keyId": {
            "type": "string",
            "description": "Required for rotate, revoke, update-key-scopes."
          },
          "label": {
            "type": "string",
            "description": "For generate-key."
          },
          "name": {
            "type": "string",
            "description": "Alias for label on generate-key."
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Scope group ids from x-portal-ops-scope-groups, or \"*\" for legacy full access."
          },
          "grantTtlMode": {
            "type": "string",
            "enum": [
              "until_revoked",
              "expires_after_days"
            ],
            "description": "For save-mcp-connection. until_revoked (default) — OAuth refresh family lasts until the Ops key is revoked or rotated. expires_after_days — cap lifetime from each new ChatGPT Connect."
          },
          "expiresAfterDays": {
            "type": "integer",
            "minimum": 1,
            "maximum": 3650,
            "description": "Required when grantTtlMode is expires_after_days (new connections only)."
          }
        }
      },
      "PortalMcpConnectionSettings": {
        "type": "object",
        "description": "Org ChatGPT / MCP OAuth refresh-family deadline (Settings → Portal Ops keys).",
        "properties": {
          "grantTtlMode": {
            "type": "string",
            "enum": [
              "until_revoked",
              "expires_after_days"
            ]
          },
          "expiresAfterDays": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Days from each new Connect when grantTtlMode is expires_after_days; null when until_revoked."
          }
        }
      },
      "OpsBriefKeySuccess": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "orgId": {
            "type": "string"
          },
          "keys": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "availableScopes": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "mcpConnection": {
            "$ref": "#/components/schemas/PortalMcpConnectionSettings"
          },
          "key": {
            "type": "string",
            "description": "Plaintext ppops key — shown once on generate/rotate."
          },
          "shownOnce": {
            "type": "boolean"
          }
        }
      },
      "PortalOpsInput_add_crew_member": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Display or job title — for jobs use PO/quote number, not customer name."
          },
          "username": {
            "type": "string",
            "description": "Lowercase login username, no spaces."
          },
          "role": {
            "type": "string",
            "enum": [
              "crew",
              "admin"
            ],
            "description": "Default to 'crew' unless admin explicitly says admin."
          }
        },
        "required": [
          "name",
          "username",
          "role"
        ],
        "title": "add_crew_member"
      },
      "PortalOpsInput_add_estimate_note": {
        "type": "object",
        "properties": {
          "estimateId": {
            "type": "string",
            "description": "Estimate document id."
          },
          "jobId": {
            "type": "string",
            "description": "Portal job id."
          },
          "jobRef": {
            "type": "string",
            "description": "Field `jobRef` — see tool handler and Estimate Builder parity."
          },
          "note": {
            "type": "string",
            "minLength": 1,
            "maxLength": 1000,
            "description": "Field `note` — see tool handler and Estimate Builder parity."
          },
          "idempotencyKey": {
            "type": "string",
            "description": "Reuse for retries of the same note; use a new value for a new handoff."
          }
        },
        "required": [
          "note"
        ],
        "title": "add_estimate_note"
      },
      "PortalOpsInput_add_job": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Job name/number — PaintScout PO/quote number like '1253', '1366', '1281'. Do NOT put the customer name here. Do NOT put the street address here when address is known."
          },
          "jobNumber": {
            "type": "string",
            "description": "Alias for name — PO/quote number."
          },
          "poNumber": {
            "type": "string",
            "description": "Alias for name — PO/quote number."
          },
          "quoteNumber": {
            "type": "string",
            "description": "Alias for name — PO/quote number."
          },
          "customerName": {
            "type": "string",
            "description": "Customer display name for contact fields only (e.g. 'Kim', 'Tricia Workman'). Never used as the job title."
          },
          "address": {
            "type": "string",
            "description": "Job site street address for maps/weather (e.g. '8510 Lagos Dr, Columbia, MO 65201'). Prefer this over stuffing the address into name."
          },
          "phone": {
            "type": "string",
            "description": "Customer phone (alias for customerPhone)."
          },
          "customerPhone": {
            "type": "string",
            "description": "Customer phone."
          },
          "email": {
            "type": "string",
            "description": "Customer email (alias for customerEmail)."
          },
          "customerEmail": {
            "type": "string",
            "description": "Customer email."
          },
          "amount": {
            "type": "number",
            "description": "Contract / sold amount in USD (writes jobCosting.soldUsd)."
          },
          "soldUsd": {
            "type": "number",
            "description": "Alias for amount."
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "accepted"
            ],
            "description": "Job status. Default active. Use accepted for sold-but-not-started."
          },
          "hoursEstimate": {
            "type": "number",
            "description": "Optional estimated total hours (whole number). Soft budget only when billingType is time_and_materials."
          },
          "notes": {
            "type": "string",
            "description": "Optional initial notes to seed the summary."
          },
          "generalJob": {
            "type": "boolean",
            "description": "True for non-customer overhead buckets like Shop. General jobs keep a normal job page but always stay active."
          },
          "billingType": {
            "type": "string",
            "enum": [
              "fixed",
              "time_and_materials"
            ],
            "description": "fixed (default) expects hours for progress %; time_and_materials may omit exact hours and shows tracked hours instead."
          },
          "pdfBase64": {
            "type": "string",
            "description": "Optional base64 PDF (or data:application/pdf;base64,…) to attach as the work order on create."
          },
          "fileName": {
            "type": "string",
            "description": "Optional filename for pdfBase64 (e.g. work-order-1253-Kim.pdf)."
          }
        },
        "required": [
          "name"
        ],
        "title": "add_job"
      },
      "PortalOpsInput_add_reminder": {
        "type": "object",
        "properties": {
          "recipients": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Array of crew member display names (exactly as they appear in list_crew). Use ['everyone'] to broadcast to all active crew. Use ['me'] to send to the admin themselves (for personal reminders)."
          },
          "title": {
            "type": "string",
            "description": "Short bold headline, 4-8 words. E.g. 'Grab the sprayer', 'Team meeting at 3pm', 'Clock out reminder'."
          },
          "body": {
            "type": "string",
            "description": "One or two short sentences with the details. E.g. 'Need the Graco 395 on the Johnson job before noon.'"
          },
          "reason": {
            "type": "string",
            "description": "Brief explanation of why this reminder is being sent — for the admin audit log. E.g. 'admin asked to remind crew about meeting'."
          }
        },
        "required": [
          "recipients",
          "title",
          "body"
        ],
        "title": "add_reminder"
      },
      "PortalOpsInput_add_shift_manual": {
        "type": "object",
        "properties": {
          "userName": {
            "type": "string",
            "description": "Crew member's display name, exactly as in list_crew."
          },
          "jobName": {
            "type": "string",
            "description": "Job name, exactly as in list_jobs. Alternatively pass jobId."
          },
          "jobId": {
            "type": "string",
            "description": "Job ID from list_jobs — use this if there are two jobs with similar names."
          },
          "clockInTs": {
            "type": "number",
            "description": "Clock-in time as JS milliseconds epoch."
          },
          "clockOutTs": {
            "type": "number",
            "description": "Optional clock-out time as JS milliseconds epoch. Omit if user is still on the clock."
          },
          "reason": {
            "type": "string",
            "description": "Audit-log reason, e.g. 'Ben forgot to clock in Tuesday morning'."
          }
        },
        "required": [
          "userName",
          "clockInTs",
          "reason"
        ],
        "title": "add_shift_manual"
      },
      "PortalOpsInput_add_spot_assignment": {
        "type": "object",
        "properties": {
          "jobId": {
            "type": "string",
            "description": "Job ID from list_jobs."
          },
          "jobName": {
            "type": "string",
            "description": "Alternatively the job's name, exactly as in list_jobs."
          },
          "userName": {
            "type": "string",
            "description": "Crew member's display name, exactly as in list_crew."
          },
          "start": {
            "type": "string",
            "description": "First day of the spot assignment, YYYY-MM-DD."
          },
          "end": {
            "type": "string",
            "description": "Last day of the spot assignment, YYYY-MM-DD. Omit for a single day (defaults to start)."
          },
          "note": {
            "type": "string",
            "description": "Optional short label, e.g. 'finish trim'."
          }
        },
        "required": [
          "userName",
          "start"
        ],
        "title": "add_spot_assignment"
      },
      "PortalOpsInput_add_task_to_job": {
        "type": "object",
        "properties": {
          "jobId": {
            "type": "string",
            "description": "Portal job id."
          },
          "task": {
            "type": "string",
            "description": "Short task text."
          }
        },
        "required": [
          "jobId",
          "task"
        ],
        "title": "add_task_to_job"
      },
      "PortalOpsInput_add_tool": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Tool name — e.g. 'Little Giant', 'Titan 840i', '6ft Extension Pole'."
          },
          "category": {
            "type": "string",
            "description": "Optional category — e.g. 'Ladders', 'Sprayers', 'Power Tools', 'Poles', 'Prep', 'Paint Tools'."
          },
          "notes": {
            "type": "string",
            "description": "Optional notes — serial, owner, where it lives."
          }
        },
        "required": [
          "name"
        ],
        "title": "add_tool"
      },
      "PortalOpsInput_advise_cfo": {
        "type": "object",
        "properties": {
          "period": {
            "type": "string",
            "enum": [
              "this_month",
              "last_month"
            ],
            "description": "Reporting period. Default this_month."
          },
          "limit": {
            "type": "number",
            "description": "Max actions. Default 40, max 200."
          }
        },
        "title": "advise_cfo"
      },
      "PortalOpsInput_analyze_numbers": {
        "type": "object",
        "properties": {
          "windowDays": {
            "type": "number",
            "description": "Sales KPI window in days. Default 30. Max 366."
          }
        },
        "title": "analyze_numbers"
      },
      "PortalOpsInput_analyze_pipeline": {
        "type": "object",
        "properties": {
          "stuckDays": {
            "type": "number",
            "description": "Flag deals stuck longer than this many days. Default 7."
          }
        },
        "title": "analyze_pipeline"
      },
      "PortalOpsInput_analyze_rate_book": {
        "type": "object",
        "properties": {},
        "title": "analyze_rate_book"
      },
      "PortalOpsInput_analyze_time_trends": {
        "type": "object",
        "properties": {
          "weeks": {
            "type": "number",
            "description": "Number of recent weeks to analyze. Default 6. Max 12."
          }
        },
        "title": "analyze_time_trends"
      },
      "PortalOpsInput_annotate_photo": {
        "type": "object",
        "properties": {
          "sourcePhotoId": {
            "type": "string",
            "description": "Assistant photo id (from upload_photo or list_photos)."
          },
          "jobId": {
            "type": "string",
            "description": "With sourceStoragePath / sourceUrl to annotate an existing job photo."
          },
          "sourceStoragePath": {
            "type": "string",
            "description": "Field `sourceStoragePath` — see tool handler and Estimate Builder parity."
          },
          "sourceUrl": {
            "type": "string",
            "description": "Field `sourceUrl` — see tool handler and Estimate Builder parity."
          },
          "imageBase64": {
            "type": "string",
            "description": "Annotated image bytes, base64."
          },
          "contentType": {
            "type": "string",
            "description": "Field `contentType` — see tool handler and Estimate Builder parity."
          },
          "caption": {
            "type": "string",
            "description": "Field `caption` — see tool handler and Estimate Builder parity."
          },
          "room": {
            "type": "string",
            "description": "Field `room` — see tool handler and Estimate Builder parity."
          },
          "annotationNotes": {
            "type": "string",
            "description": "What the markup shows (staff-only)."
          },
          "fileName": {
            "type": "string",
            "description": "Field `fileName` — see tool handler and Estimate Builder parity."
          },
          "photoId": {
            "type": "string",
            "description": "Source photoId from list_photos (assistant id or job storage path)."
          },
          "storagePath": {
            "type": "string",
            "description": "Existing job photo storage path."
          },
          "annotatedImageBase64": {
            "type": "string",
            "description": "Annotated image bytes; alternative to imageBase64."
          },
          "notes": {
            "type": "string",
            "description": "Staff-only annotation notes."
          },
          "scope": {
            "type": "string",
            "enum": [
              "job",
              "estimate"
            ],
            "description": "Field `scope` — see tool handler and Estimate Builder parity."
          }
        },
        "required": [],
        "title": "annotate_photo"
      },
      "PortalOpsInput_assign_to_job": {
        "type": "object",
        "properties": {
          "jobId": {
            "type": "string",
            "description": "Portal job id."
          },
          "mode": {
            "type": "string",
            "enum": [
              "replace",
              "add",
              "remove"
            ],
            "description": "Default is 'add'."
          },
          "users": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Exact user display names to assign."
          },
          "teams": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Team ids to assign. The client will convert these to 'team:{id}' sentinels."
          }
        },
        "required": [
          "jobId"
        ],
        "title": "assign_to_job"
      },
      "PortalOpsInput_attach_job_file": {
        "type": "object",
        "properties": {
          "jobId": {
            "type": "string",
            "description": "Existing job id from add_job / list_jobs."
          },
          "pdfBase64": {
            "type": "string",
            "description": "Base64 PDF bytes (or data:application/pdf;base64,…)."
          },
          "fileBase64": {
            "type": "string",
            "description": "Alias for pdfBase64."
          },
          "fileName": {
            "type": "string",
            "description": "Filename, e.g. work-order-1310-Kim.pdf."
          },
          "kind": {
            "type": "string",
            "description": "Optional; default work-order."
          }
        },
        "required": [
          "jobId",
          "pdfBase64"
        ],
        "title": "attach_job_file"
      },
      "PortalOpsInput_attach_work_order": {
        "type": "object",
        "properties": {
          "jobId": {
            "type": "string",
            "description": "Existing job id from add_job / list_jobs."
          },
          "pdfBase64": {
            "type": "string",
            "description": "Base64 PDF bytes (or data:application/pdf;base64,…)."
          },
          "fileBase64": {
            "type": "string",
            "description": "Alias for pdfBase64."
          },
          "fileName": {
            "type": "string",
            "description": "Filename, e.g. work-order-1310-Kim.pdf."
          },
          "kind": {
            "type": "string",
            "description": "Optional; default work-order."
          }
        },
        "required": [
          "jobId",
          "pdfBase64"
        ],
        "title": "attach_work_order"
      },
      "PortalOpsInput_book_client_visit": {
        "type": "object",
        "properties": {
          "jobId": {
            "type": "string",
            "description": "Portal job id."
          },
          "jobName": {
            "type": "string",
            "description": "Field `jobName` — see tool handler and Estimate Builder parity."
          },
          "startAt": {
            "type": "number",
            "description": "Field `startAt` — see tool handler and Estimate Builder parity."
          },
          "endAt": {
            "type": "number",
            "description": "Field `endAt` — see tool handler and Estimate Builder parity."
          },
          "visitKind": {
            "type": "string",
            "description": "Field `visitKind` — see tool handler and Estimate Builder parity."
          },
          "visitId": {
            "type": "string",
            "description": "Optional — update existing client visit id."
          },
          "estimatorUserId": {
            "type": "string",
            "description": "Field `estimatorUserId` — see tool handler and Estimate Builder parity."
          },
          "estimatorName": {
            "type": "string",
            "description": "Field `estimatorName` — see tool handler and Estimate Builder parity."
          },
          "notes": {
            "type": "string",
            "description": "Field `notes` — see tool handler and Estimate Builder parity."
          },
          "customerConfirmed": {
            "type": "boolean",
            "description": "Required true on Portal Ops keys: the customer confirmed this time."
          },
          "customerConfirmationNote": {
            "type": "string",
            "description": "Field `customerConfirmationNote` — see tool handler and Estimate Builder parity."
          },
          "notifyCustomer": {
            "type": "boolean",
            "description": "Only when explicitly asked: allow the org's automatic visit reminders. Default off for keys."
          }
        },
        "required": [
          "startAt"
        ],
        "title": "book_client_visit"
      },
      "PortalOpsInput_book_estimate_visit": {
        "type": "object",
        "properties": {
          "jobId": {
            "type": "string",
            "description": "Portal job id."
          },
          "jobName": {
            "type": "string",
            "description": "Field `jobName` — see tool handler and Estimate Builder parity."
          },
          "startAt": {
            "type": "number",
            "description": "Visit start — epoch ms or ISO string."
          },
          "endAt": {
            "type": "number",
            "description": "Visit end — epoch ms or ISO (default +60m)."
          },
          "estimatorUserId": {
            "type": "string",
            "description": "Field `estimatorUserId` — see tool handler and Estimate Builder parity."
          },
          "estimatorName": {
            "type": "string",
            "description": "Field `estimatorName` — see tool handler and Estimate Builder parity."
          },
          "syncGoogle": {
            "type": "boolean",
            "description": "Write estimator Google Calendar (default true)."
          },
          "notes": {
            "type": "string",
            "description": "Field `notes` — see tool handler and Estimate Builder parity."
          },
          "customerConfirmed": {
            "type": "boolean",
            "description": "Required true on Portal Ops keys: the customer confirmed this time."
          },
          "customerConfirmationNote": {
            "type": "string",
            "description": "How / when the customer confirmed (kept in schedule history)."
          },
          "notifyCustomer": {
            "type": "boolean",
            "description": "Only when explicitly asked: let the org's normal automatic visit reminders go to the customer. Default off for keys."
          }
        },
        "required": [
          "startAt"
        ],
        "title": "book_estimate_visit"
      },
      "PortalOpsInput_cancel_client_visit": {
        "type": "object",
        "properties": {
          "jobId": {
            "type": "string",
            "description": "Portal job id."
          },
          "jobName": {
            "type": "string",
            "description": "Field `jobName` — see tool handler and Estimate Builder parity."
          },
          "visitId": {
            "type": "string",
            "description": "Field `visitId` — see tool handler and Estimate Builder parity."
          }
        },
        "required": [
          "visitId"
        ],
        "title": "cancel_client_visit"
      },
      "PortalOpsInput_cancel_estimate_visit": {
        "type": "object",
        "properties": {
          "jobId": {
            "type": "string",
            "description": "Portal job id."
          },
          "jobName": {
            "type": "string",
            "description": "Field `jobName` — see tool handler and Estimate Builder parity."
          }
        },
        "required": [],
        "title": "cancel_estimate_visit"
      },
      "PortalOpsInput_cfo_setup_status": {
        "type": "object",
        "properties": {
          "wages": {
            "type": "array",
            "description": "Optional wage samples already gathered in chat (name + hourlyWageUsd or annualSalaryUsd).",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string",
                  "description": "Display or job title — for jobs use PO/quote number, not customer name."
                },
                "userId": {
                  "type": "string",
                  "description": "Field `userId` — see tool handler and Estimate Builder parity."
                },
                "hourlyWageUsd": {
                  "type": "number",
                  "description": "Field `hourlyWageUsd` — see tool handler and Estimate Builder parity."
                },
                "annualSalaryUsd": {
                  "type": "number",
                  "description": "Field `annualSalaryUsd` — see tool handler and Estimate Builder parity."
                },
                "weeklyPayUsd": {
                  "type": "number",
                  "description": "Field `weeklyPayUsd` — see tool handler and Estimate Builder parity."
                }
              }
            }
          }
        },
        "title": "cfo_setup_status"
      },
      "PortalOpsInput_check_in_tool": {
        "type": "object",
        "properties": {
          "toolId": {
            "type": "string",
            "description": "Field `toolId` — see tool handler and Estimate Builder parity."
          },
          "byName": {
            "type": "string",
            "description": "Field `byName` — see tool handler and Estimate Builder parity."
          },
          "note": {
            "type": "string",
            "description": "Field `note` — see tool handler and Estimate Builder parity."
          }
        },
        "required": [
          "toolId"
        ],
        "title": "check_in_tool"
      },
      "PortalOpsInput_check_out_tool": {
        "type": "object",
        "properties": {
          "toolId": {
            "type": "string",
            "description": "Field `toolId` — see tool handler and Estimate Builder parity."
          },
          "jobId": {
            "type": "string",
            "description": "Portal job id."
          },
          "byName": {
            "type": "string",
            "description": "Optional — name of the person who took it (defaults to sender)."
          },
          "note": {
            "type": "string",
            "description": "Optional context — e.g. 'via chat in 1297'."
          }
        },
        "required": [
          "toolId",
          "jobId"
        ],
        "title": "check_out_tool"
      },
      "PortalOpsInput_clock_in_user": {
        "type": "object",
        "properties": {
          "userName": {
            "type": "string",
            "description": "Crew member's display name, exactly as in list_crew."
          },
          "jobName": {
            "type": "string",
            "description": "Active job's name, exactly as in list_jobs. Alternatively pass jobId."
          },
          "jobId": {
            "type": "string",
            "description": "Job ID from list_jobs — use this if there are two jobs with similar names."
          }
        },
        "required": [
          "userName"
        ],
        "title": "clock_in_user"
      },
      "PortalOpsInput_clock_out_user": {
        "type": "object",
        "properties": {
          "userName": {
            "type": "string",
            "description": "Crew member's display name, exactly as in list_crew."
          }
        },
        "required": [
          "userName"
        ],
        "title": "clock_out_user"
      },
      "PortalOpsInput_commit_app_import": {
        "type": "object",
        "properties": {
          "previewId": {
            "type": "string",
            "description": "previewId from preview_app_import (aip_…)."
          }
        },
        "required": [
          "previewId"
        ],
        "title": "commit_app_import"
      },
      "PortalOpsInput_confirm_receipt": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "receiptId from ingest_receipt / list_receipts."
          },
          "jobId": {
            "type": "string",
            "description": "Optional. Must equal the one matched job."
          }
        },
        "required": [
          "id"
        ],
        "title": "confirm_receipt"
      },
      "PortalOpsInput_create_drip_sequence": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Display or job title — for jobs use PO/quote number, not customer name."
          },
          "enabled": {
            "type": "boolean",
            "description": "Field `enabled` — see tool handler and Estimate Builder parity."
          },
          "trigger": {
            "type": "object",
            "description": "Field `trigger` — see tool handler and Estimate Builder parity."
          },
          "cancelOnEvents": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Field `cancelOnEvents` — see tool handler and Estimate Builder parity."
          },
          "steps": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Field `steps` — see tool handler and Estimate Builder parity."
          }
        },
        "required": [
          "name",
          "trigger",
          "steps"
        ],
        "title": "create_drip_sequence"
      },
      "PortalOpsInput_create_estimate": {
        "type": "object",
        "properties": {
          "jobId": {
            "type": "string",
            "description": "Existing Portal job id (list_sales_leads / create_job / read_job)."
          },
          "estimateType": {
            "type": "string",
            "description": "Interior | Exterior | etc. Default Interior."
          },
          "customerName": {
            "type": "string",
            "description": "Optional override; defaults from job."
          },
          "customerEmail": {
            "type": "string",
            "description": "Optional override."
          },
          "customerPhone": {
            "type": "string",
            "description": "Optional override."
          },
          "address": {
            "type": "string",
            "description": "Optional job site override."
          },
          "contactId": {
            "type": "string",
            "description": "Optional CRM contact id."
          }
        },
        "required": [
          "jobId"
        ],
        "title": "create_estimate"
      },
      "PortalOpsInput_create_job": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Job name/number — PaintScout PO/quote number like '1253', '1366', '1281'. Do NOT put the customer name here. Do NOT put the street address here when address is known."
          },
          "jobNumber": {
            "type": "string",
            "description": "Alias for name — PO/quote number."
          },
          "poNumber": {
            "type": "string",
            "description": "Alias for name — PO/quote number."
          },
          "quoteNumber": {
            "type": "string",
            "description": "Alias for name — PO/quote number."
          },
          "customerName": {
            "type": "string",
            "description": "Customer display name for contact fields only (e.g. 'Kim', 'Tricia Workman'). Never used as the job title."
          },
          "address": {
            "type": "string",
            "description": "Job site street address for maps/weather (e.g. '8510 Lagos Dr, Columbia, MO 65201'). Prefer this over stuffing the address into name."
          },
          "phone": {
            "type": "string",
            "description": "Customer phone (alias for customerPhone)."
          },
          "customerPhone": {
            "type": "string",
            "description": "Customer phone."
          },
          "email": {
            "type": "string",
            "description": "Customer email (alias for customerEmail)."
          },
          "customerEmail": {
            "type": "string",
            "description": "Customer email."
          },
          "amount": {
            "type": "number",
            "description": "Contract / sold amount in USD (writes jobCosting.soldUsd)."
          },
          "soldUsd": {
            "type": "number",
            "description": "Alias for amount."
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "accepted"
            ],
            "description": "Job status. Default active. Use accepted for sold-but-not-started."
          },
          "hoursEstimate": {
            "type": "number",
            "description": "Optional estimated total hours (whole number). Soft budget only when billingType is time_and_materials."
          },
          "notes": {
            "type": "string",
            "description": "Optional initial notes to seed the summary."
          },
          "generalJob": {
            "type": "boolean",
            "description": "True for non-customer overhead buckets like Shop. General jobs keep a normal job page but always stay active."
          },
          "billingType": {
            "type": "string",
            "enum": [
              "fixed",
              "time_and_materials"
            ],
            "description": "fixed (default) expects hours for progress %; time_and_materials may omit exact hours and shows tracked hours instead."
          },
          "pdfBase64": {
            "type": "string",
            "description": "Optional base64 PDF (or data:application/pdf;base64,…) to attach as the work order on create."
          },
          "fileName": {
            "type": "string",
            "description": "Optional filename for pdfBase64 (e.g. work-order-1253-Kim.pdf)."
          }
        },
        "required": [
          "name"
        ],
        "title": "create_job"
      },
      "PortalOpsInput_create_lead": {
        "type": "object",
        "properties": {
          "phone": {
            "type": "string",
            "description": "Caller phone, 10 digits (formatting ok)."
          },
          "name": {
            "type": "string",
            "description": "Caller name."
          },
          "address": {
            "type": "string",
            "description": "Job site address if they gave one."
          },
          "notes": {
            "type": "string",
            "description": "What they want painted / when."
          }
        },
        "required": [
          "phone"
        ],
        "title": "create_lead"
      },
      "PortalOpsInput_create_recurring_reminder": {
        "type": "object",
        "properties": {
          "title": {
            "type": "string",
            "description": "Short bold headline, 4-8 words. E.g. 'Morning walkthrough', 'Clock out reminder', 'Tool check'."
          },
          "body": {
            "type": "string",
            "description": "One or two short sentences with the details of what to do."
          },
          "schedule": {
            "type": "object",
            "description": "When the reminder fires.",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "daily",
                  "weekly",
                  "interval"
                ],
                "description": "'daily' = every WORK DAY (Mon-Fri) at hour:minute (skips Sat/Sun). 'weekly' = fires only on specific days-of-week at hour:minute (use this if admin wants weekends included or a subset of days). 'interval' = every N days at hour:minute regardless of weekday."
              },
              "hour": {
                "type": "integer",
                "description": "0-23 local hour (24h clock). E.g. 7 for 7am, 17 for 5pm."
              },
              "minute": {
                "type": "integer",
                "description": "0-59 local minute. Default 0 if not specified."
              },
              "daysOfWeek": {
                "type": "array",
                "items": {
                  "type": "integer"
                },
                "description": "For weekly only. 0=Sunday, 1=Monday, ... 6=Saturday. E.g. [1,2,3,4,5] for weekdays."
              },
              "intervalDays": {
                "type": "integer",
                "description": "For interval only. 1 = every day (use 'daily' instead), 2 = every other day, 7 = weekly-ish, etc."
              }
            },
            "required": [
              "type",
              "hour"
            ]
          },
          "targets": {
            "type": "object",
            "description": "Who gets the reminder.",
            "properties": {
              "roles": {
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": [
                    "admin",
                    "lead",
                    "crew",
                    "everyone"
                  ]
                },
                "description": "Target by role. 'lead' = crew leads. 'everyone' = all active users regardless of role. Use this when admin says 'remind the crew' / 'remind everyone' / 'remind the leads'."
              },
              "userIds": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Specific user ids from list_crew. Use this when admin names specific people. Call list_crew first if you don't have ids."
              }
            }
          },
          "filter": {
            "type": "string",
            "enum": [
              "none",
              "still-on-clock",
              "no-shift-today"
            ],
            "description": "Optional live-state filter applied at fire time. 'none' (default) = send to everyone in targets. 'still-on-clock' = only users currently clocked in (open shift). 'no-shift-today' = only users with no shift started today yet."
          },
          "enabled": {
            "type": "boolean",
            "description": "Default true. Set to false to create it paused."
          }
        },
        "required": [
          "title",
          "body",
          "schedule",
          "targets"
        ],
        "title": "create_recurring_reminder"
      },
      "PortalOpsInput_create_webhook": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "description": "Public https URL (private / internal hosts are rejected)."
          },
          "events": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "One or more event names."
          },
          "label": {
            "type": "string",
            "description": "Human-readable key label in Settings."
          }
        },
        "required": [
          "url",
          "events"
        ],
        "title": "create_webhook"
      },
      "PortalOpsInput_delete_estimate_descriptor": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Field `id` — see tool handler and Estimate Builder parity."
          },
          "descriptorId": {
            "type": "string",
            "description": "Field `descriptorId` — see tool handler and Estimate Builder parity."
          }
        },
        "required": [
          "id"
        ],
        "title": "delete_estimate_descriptor"
      },
      "PortalOpsInput_delete_estimate_product": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Field `id` — see tool handler and Estimate Builder parity."
          },
          "productId": {
            "type": "string",
            "description": "Field `productId` — see tool handler and Estimate Builder parity."
          }
        },
        "required": [
          "id"
        ],
        "title": "delete_estimate_product"
      },
      "PortalOpsInput_delete_job_file": {
        "type": "object",
        "properties": {
          "jobId": {
            "type": "string",
            "description": "Existing job id (e.g. from list_jobs / read_job)."
          },
          "storagePath": {
            "type": "string",
            "description": "Preferred — full orgs/{orgId}/jobs/{jobId}/{ts}_{fileName} path, or a unique path fragment / timestamp prefix."
          },
          "path": {
            "type": "string",
            "description": "Alias for storagePath."
          },
          "filePath": {
            "type": "string",
            "description": "Alias for storagePath."
          },
          "fileName": {
            "type": "string",
            "description": "Exact file name on job.files[] (e.g. po-1349-marlene-vogler.pdf). Never a substring — TEXT WOs must not match raster names."
          },
          "name": {
            "type": "string",
            "description": "Alias for fileName."
          },
          "fileId": {
            "type": "string",
            "description": "Optional id or storage timestamp prefix (e.g. 1788595995663)."
          }
        },
        "required": [
          "jobId"
        ],
        "title": "delete_job_file"
      },
      "PortalOpsInput_delete_production_rate": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Field `id` — see tool handler and Estimate Builder parity."
          },
          "rateId": {
            "type": "string",
            "description": "Field `rateId` — see tool handler and Estimate Builder parity."
          }
        },
        "required": [
          "id"
        ],
        "title": "delete_production_rate"
      },
      "PortalOpsInput_delete_recurring_reminder": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Field `id` — see tool handler and Estimate Builder parity."
          }
        },
        "required": [
          "id"
        ],
        "title": "delete_recurring_reminder"
      },
      "PortalOpsInput_delete_shift": {
        "type": "object",
        "properties": {
          "shiftId": {
            "type": "string",
            "description": "Shift ID from the TIME CLOCK block."
          },
          "reason": {
            "type": "string",
            "description": "Audit-log reason for the deletion, e.g. 'duplicate of the shift right before it'."
          }
        },
        "required": [
          "shiftId"
        ],
        "title": "delete_shift"
      },
      "PortalOpsInput_delete_tool": {
        "type": "object",
        "properties": {
          "toolId": {
            "type": "string",
            "description": "Field `toolId` — see tool handler and Estimate Builder parity."
          }
        },
        "required": [
          "toolId"
        ],
        "title": "delete_tool"
      },
      "PortalOpsInput_delete_webhook": {
        "type": "object",
        "properties": {
          "webhookId": {
            "type": "string",
            "description": "Field `webhookId` — see tool handler and Estimate Builder parity."
          }
        },
        "required": [
          "webhookId"
        ],
        "title": "delete_webhook"
      },
      "PortalOpsInput_detach_job_file": {
        "type": "object",
        "properties": {
          "jobId": {
            "type": "string",
            "description": "Existing job id (e.g. from list_jobs / read_job)."
          },
          "storagePath": {
            "type": "string",
            "description": "Preferred — full orgs/{orgId}/jobs/{jobId}/{ts}_{fileName} path, or a unique path fragment / timestamp prefix."
          },
          "path": {
            "type": "string",
            "description": "Alias for storagePath."
          },
          "filePath": {
            "type": "string",
            "description": "Alias for storagePath."
          },
          "fileName": {
            "type": "string",
            "description": "Exact file name on job.files[] (e.g. po-1349-marlene-vogler.pdf). Never a substring — TEXT WOs must not match raster names."
          },
          "name": {
            "type": "string",
            "description": "Alias for fileName."
          },
          "fileId": {
            "type": "string",
            "description": "Optional id or storage timestamp prefix (e.g. 1788595995663)."
          }
        },
        "required": [
          "jobId"
        ],
        "title": "detach_job_file"
      },
      "PortalOpsInput_download_photo": {
        "type": "object",
        "properties": {
          "scope": {
            "type": "string",
            "enum": [
              "job",
              "estimate",
              "checkin"
            ],
            "description": "Field `scope` — see tool handler and Estimate Builder parity."
          },
          "photoId": {
            "type": "string",
            "description": "Field `photoId` — see tool handler and Estimate Builder parity."
          },
          "jobId": {
            "type": "string",
            "description": "Portal job id."
          },
          "estimateId": {
            "type": "string",
            "description": "Estimate document id."
          },
          "checkinId": {
            "type": "string",
            "description": "Field `checkinId` — see tool handler and Estimate Builder parity."
          },
          "variant": {
            "type": "string",
            "enum": [
              "original",
              "annotated"
            ],
            "description": "Field `variant` — see tool handler and Estimate Builder parity."
          },
          "includeBase64": {
            "type": "boolean",
            "description": "Opt-in true for inline pdfBase64; default is signed downloadUrl only (~15m)."
          }
        },
        "required": [
          "photoId"
        ],
        "title": "download_photo"
      },
      "PortalOpsInput_download_proposal_pdf": {
        "type": "object",
        "properties": {
          "estimateId": {
            "type": "string",
            "description": "Estimate document id."
          },
          "jobId": {
            "type": "string",
            "description": "Portal job id."
          },
          "includeBase64": {
            "type": "boolean",
            "description": "Default false. When true, includes pdfBase64 (large)."
          }
        },
        "title": "download_proposal_pdf"
      },
      "PortalOpsInput_draft_cfo_report": {
        "type": "object",
        "properties": {
          "packIds": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "month",
                "job_costing",
                "budget_variance",
                "job_type",
                "crew_costing",
                "hours_kpis",
                "sales_pulse",
                "ar_cash",
                "underpriced",
                "advisor_actions",
                "cash",
                "pl_actual",
                "bank_uncoded",
                "close_month"
              ]
            },
            "description": "Optional pack ids to include."
          },
          "period": {
            "type": "string",
            "enum": [
              "this_month",
              "last_month"
            ],
            "description": "Reporting period. Default this_month."
          }
        },
        "title": "draft_cfo_report"
      },
      "PortalOpsInput_draft_drip_sequence": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Display or job title — for jobs use PO/quote number, not customer name."
          },
          "instruction": {
            "type": "string",
            "description": "Field `instruction` — see tool handler and Estimate Builder parity."
          },
          "enabled": {
            "type": "boolean",
            "description": "Field `enabled` — see tool handler and Estimate Builder parity."
          },
          "trigger": {
            "type": "object",
            "description": "Event trigger {type:'event', event:'proposal.sent'} or idle {type:'stage_idle', stage:'waiting', idleDays:7}.",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "event",
                  "stage_idle"
                ],
                "description": "Field `type` — see tool handler and Estimate Builder parity."
              },
              "event": {
                "type": "string",
                "description": "Field `event` — see tool handler and Estimate Builder parity."
              },
              "events": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Field `events` — see tool handler and Estimate Builder parity."
              },
              "jobStatusTo": {
                "type": "string",
                "description": "Field `jobStatusTo` — see tool handler and Estimate Builder parity."
              },
              "stage": {
                "type": "string",
                "description": "Field `stage` — see tool handler and Estimate Builder parity."
              },
              "idleDays": {
                "type": "number",
                "description": "Field `idleDays` — see tool handler and Estimate Builder parity."
              }
            }
          },
          "cancelOnEvents": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Field `cancelOnEvents` — see tool handler and Estimate Builder parity."
          },
          "steps": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "delayMinutes": {
                  "type": "number",
                  "description": "Field `delayMinutes` — see tool handler and Estimate Builder parity."
                },
                "channel": {
                  "type": "string",
                  "enum": [
                    "email",
                    "sms"
                  ],
                  "description": "Field `channel` — see tool handler and Estimate Builder parity."
                },
                "subject": {
                  "type": "string",
                  "description": "Field `subject` — see tool handler and Estimate Builder parity."
                },
                "body": {
                  "type": "string",
                  "description": "Field `body` — see tool handler and Estimate Builder parity."
                }
              }
            },
            "description": "Field `steps` — see tool handler and Estimate Builder parity."
          }
        },
        "required": [
          "name",
          "trigger",
          "steps"
        ],
        "title": "draft_drip_sequence"
      },
      "PortalOpsInput_draft_email_template": {
        "type": "object",
        "properties": {
          "kind": {
            "type": "string",
            "description": "Template key from list_email_templates."
          },
          "instruction": {
            "type": "string",
            "description": "Short note of what the admin asked to change."
          },
          "subject": {
            "type": "string",
            "description": "Field `subject` — see tool handler and Estimate Builder parity."
          },
          "heading": {
            "type": "string",
            "description": "Field `heading` — see tool handler and Estimate Builder parity."
          },
          "body": {
            "type": "string",
            "description": "Field `body` — see tool handler and Estimate Builder parity."
          },
          "ctaLabel": {
            "type": "string",
            "description": "Field `ctaLabel` — see tool handler and Estimate Builder parity."
          },
          "sms": {
            "type": "string",
            "description": "Field `sms` — see tool handler and Estimate Builder parity."
          }
        },
        "required": [
          "kind",
          "subject",
          "body"
        ],
        "title": "draft_email_template"
      },
      "PortalOpsInput_draft_followup": {
        "type": "object",
        "properties": {
          "jobId": {
            "type": "string",
            "description": "Job id to draft follow-up for."
          }
        },
        "required": [
          "jobId"
        ],
        "title": "draft_followup"
      },
      "PortalOpsInput_edit_crew_member": {
        "type": "object",
        "properties": {
          "userId": {
            "type": "string",
            "description": "Field `userId` — see tool handler and Estimate Builder parity."
          },
          "name": {
            "type": "string",
            "description": "Display or job title — for jobs use PO/quote number, not customer name."
          },
          "username": {
            "type": "string",
            "description": "Field `username` — see tool handler and Estimate Builder parity."
          },
          "role": {
            "type": "string",
            "enum": [
              "crew",
              "admin"
            ],
            "description": "Field `role` — see tool handler and Estimate Builder parity."
          }
        },
        "required": [
          "userId"
        ],
        "title": "edit_crew_member"
      },
      "PortalOpsInput_edit_job": {
        "type": "object",
        "properties": {
          "jobId": {
            "type": "string",
            "description": "Portal job id."
          },
          "name": {
            "type": "string",
            "description": "Display or job title — for jobs use PO/quote number, not customer name."
          },
          "address": {
            "type": "string",
            "description": "Update the job site street address (maps/weather). Prefer this over putting the address in name."
          },
          "hoursEstimate": {
            "type": "number",
            "description": "Field `hoursEstimate` — see tool handler and Estimate Builder parity."
          },
          "status": {
            "type": "string",
            "enum": [
              "estimate",
              "active",
              "accepted",
              "limbo",
              "completed",
              "archived",
              "complete"
            ],
            "description": "Field `status` — see tool handler and Estimate Builder parity."
          },
          "limboReason": {
            "type": "string",
            "description": "Optional staff note for why the job is in limbo (waiting on trades, punch list, seasonal hold such as waiting until fall so a lift doesn't ruin the grass). Ignored unless status is limbo or the job is already limbo."
          },
          "generalJob": {
            "type": "boolean",
            "description": "True makes this a general/overhead job that always remains active; false returns it to normal status behavior."
          },
          "billingType": {
            "type": "string",
            "enum": [
              "fixed",
              "time_and_materials"
            ],
            "description": "fixed (default) or time_and_materials for jobs without a required hours estimate."
          },
          "soldTotal": {
            "type": "number",
            "minimum": 0,
            "description": "Sold / contract total in dollars for a job with NO Portal estimate (e.g. sold in PaintScout). Finite number >= 0, rounded to cents; 0 clears it. Invoices and the balance use it only when the job has no Portal estimate — a job with an estimate keeps billing from the estimate. Stored off the job doc (admin/office only). ADMIN ONLY — refused for non-admins. NEVER invent money."
          }
        },
        "required": [
          "jobId"
        ],
        "title": "edit_job"
      },
      "PortalOpsInput_edit_job_schedule": {
        "type": "object",
        "properties": {
          "jobId": {
            "type": "string",
            "description": "Job ID from list_jobs."
          },
          "jobName": {
            "type": "string",
            "description": "Alternatively the job's name, exactly as in list_jobs."
          },
          "startDate": {
            "type": "string",
            "description": "Scheduled start date, YYYY-MM-DD."
          },
          "endDate": {
            "type": "string",
            "description": "Scheduled finish date, YYYY-MM-DD. Must be on or after startDate."
          },
          "estimatedHours": {
            "type": "number",
            "description": "Optional total estimated hours for the job."
          },
          "crewLeadName": {
            "type": "string",
            "description": "Optional crew lead display name, exactly as in list_crew."
          },
          "notBefore": {
            "type": "string",
            "description": "Optional earliest-start date the job may begin (YYYY-MM-DD). Use for 'can't start before X'."
          },
          "notes": {
            "type": "string",
            "description": "Optional short scheduling note."
          },
          "datesLocked": {
            "type": "boolean",
            "description": "Optional. True pins the dates so the auto-scheduler / cascade won't move them."
          }
        },
        "required": [
          "jobId"
        ],
        "title": "edit_job_schedule"
      },
      "PortalOpsInput_edit_recurring_reminder": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Reminder id from list_recurring_reminders."
          },
          "title": {
            "type": "string",
            "description": "Field `title` — see tool handler and Estimate Builder parity."
          },
          "body": {
            "type": "string",
            "description": "Field `body` — see tool handler and Estimate Builder parity."
          },
          "schedule": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "daily",
                  "weekly",
                  "interval"
                ],
                "description": "Field `type` — see tool handler and Estimate Builder parity."
              },
              "hour": {
                "type": "integer",
                "description": "Field `hour` — see tool handler and Estimate Builder parity."
              },
              "minute": {
                "type": "integer",
                "description": "Field `minute` — see tool handler and Estimate Builder parity."
              },
              "daysOfWeek": {
                "type": "array",
                "items": {
                  "type": "integer"
                },
                "description": "Field `daysOfWeek` — see tool handler and Estimate Builder parity."
              },
              "intervalDays": {
                "type": "integer",
                "description": "Field `intervalDays` — see tool handler and Estimate Builder parity."
              }
            },
            "description": "Field `schedule` — see tool handler and Estimate Builder parity."
          },
          "targets": {
            "type": "object",
            "properties": {
              "roles": {
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": [
                    "admin",
                    "lead",
                    "crew",
                    "everyone"
                  ]
                },
                "description": "Field `roles` — see tool handler and Estimate Builder parity."
              },
              "userIds": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Field `userIds` — see tool handler and Estimate Builder parity."
              }
            },
            "description": "Field `targets` — see tool handler and Estimate Builder parity."
          },
          "filter": {
            "type": "string",
            "enum": [
              "none",
              "still-on-clock",
              "no-shift-today"
            ],
            "description": "Live-state filter. 'none' = no filter. 'still-on-clock' = only users currently clocked in. 'no-shift-today' = only users with no shift started today."
          },
          "enabled": {
            "type": "boolean",
            "description": "Field `enabled` — see tool handler and Estimate Builder parity."
          }
        },
        "required": [
          "id"
        ],
        "title": "edit_recurring_reminder"
      },
      "PortalOpsInput_edit_shift": {
        "type": "object",
        "properties": {
          "shiftId": {
            "type": "string",
            "description": "Shift ID from the TIME CLOCK block, e.g. 'abc123def456'."
          },
          "clockInTs": {
            "type": "number",
            "description": "Optional new clock-in time as JS milliseconds epoch. Omit to leave clock-in unchanged."
          },
          "clockOutTs": {
            "type": "number",
            "description": "Optional new clock-out time as JS milliseconds epoch. Omit to leave clock-out unchanged. Pass 0 to clear (re-open the shift)."
          },
          "jobId": {
            "type": "string",
            "description": "Optional new jobId if the shift should be re-assigned to a different job. Look up via list_jobs."
          },
          "reason": {
            "type": "string",
            "description": "Audit-log reason, e.g. 'Ben said he actually started at 7am not 8'."
          }
        },
        "required": [
          "shiftId",
          "reason"
        ],
        "title": "edit_shift"
      },
      "PortalOpsInput_edit_summary": {
        "type": "object",
        "properties": {
          "jobId": {
            "type": "string",
            "description": "Portal job id."
          },
          "instruction": {
            "type": "string",
            "description": "Field `instruction` — see tool handler and Estimate Builder parity."
          }
        },
        "required": [
          "jobId",
          "instruction"
        ],
        "title": "edit_summary"
      },
      "PortalOpsInput_edit_tool": {
        "type": "object",
        "properties": {
          "toolId": {
            "type": "string",
            "description": "Field `toolId` — see tool handler and Estimate Builder parity."
          },
          "name": {
            "type": "string",
            "description": "Display or job title — for jobs use PO/quote number, not customer name."
          },
          "category": {
            "type": "string",
            "description": "Field `category` — see tool handler and Estimate Builder parity."
          },
          "notes": {
            "type": "string",
            "description": "Field `notes` — see tool handler and Estimate Builder parity."
          }
        },
        "required": [
          "toolId"
        ],
        "title": "edit_tool"
      },
      "PortalOpsInput_estimate_context": {
        "type": "object",
        "properties": {
          "jobRef": {
            "type": "string",
            "description": "jobRef, or today, or YYYY-MM-DD for all visits that day."
          },
          "date": {
            "type": "string",
            "description": "Optional alias for YYYY-MM-DD day mode."
          }
        },
        "title": "estimate_context"
      },
      "PortalOpsInput_estimate_snapshot": {
        "type": "object",
        "properties": {
          "jobRef": {
            "type": "string",
            "description": "Field `jobRef` — see tool handler and Estimate Builder parity."
          },
          "jobId": {
            "type": "string",
            "description": "Portal job id."
          }
        },
        "title": "estimate_snapshot"
      },
      "PortalOpsInput_finish_bank_reconciliation": {
        "type": "object",
        "properties": {
          "recId": {
            "type": "string",
            "description": "Reconciliation id."
          },
          "accountId": {
            "type": "string",
            "description": "Bank account id."
          },
          "openingBalanceUsd": {
            "type": "number",
            "description": "Opening balance on statement."
          },
          "statementEndBalanceUsd": {
            "type": "number",
            "description": "Ending balance on statement."
          },
          "statementEndDate": {
            "type": "string",
            "description": "Statement end date YYYY-MM-DD."
          },
          "clearedTxnIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Transaction ids cleared on this statement."
          }
        },
        "required": [
          "recId"
        ],
        "title": "finish_bank_reconciliation"
      },
      "PortalOpsInput_forget": {
        "type": "object",
        "properties": {
          "factSubstring": {
            "type": "string",
            "description": "A distinctive substring from the fact to remove. Client will match by substring."
          }
        },
        "required": [
          "factSubstring"
        ],
        "title": "forget"
      },
      "PortalOpsInput_get_contact": {
        "type": "object",
        "properties": {
          "contactId": {
            "type": "string",
            "description": "Field `contactId` — see tool handler and Estimate Builder parity."
          }
        },
        "required": [
          "contactId"
        ],
        "title": "get_contact"
      },
      "PortalOpsInput_get_estimate": {
        "type": "object",
        "properties": {
          "estimateId": {
            "type": "string",
            "description": "Estimate document id."
          }
        },
        "required": [
          "estimateId"
        ],
        "title": "get_estimate"
      },
      "PortalOpsInput_get_estimate_descriptor": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Field `id` — see tool handler and Estimate Builder parity."
          },
          "descriptorId": {
            "type": "string",
            "description": "Alias for id."
          }
        },
        "title": "get_estimate_descriptor"
      },
      "PortalOpsInput_get_estimate_product": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Field `id` — see tool handler and Estimate Builder parity."
          },
          "productId": {
            "type": "string",
            "description": "Alias for id."
          }
        },
        "title": "get_estimate_product"
      },
      "PortalOpsInput_get_invoice": {
        "type": "object",
        "properties": {
          "invoiceId": {
            "type": "string",
            "description": "Field `invoiceId` — see tool handler and Estimate Builder parity."
          }
        },
        "required": [
          "invoiceId"
        ],
        "title": "get_invoice"
      },
      "PortalOpsInput_get_job_production": {
        "type": "object",
        "properties": {
          "jobId": {
            "type": "string",
            "description": "Portal job id."
          }
        },
        "required": [
          "jobId"
        ],
        "title": "get_job_production"
      },
      "PortalOpsInput_get_lead": {
        "type": "object",
        "properties": {
          "leadId": {
            "type": "string",
            "description": "Lead id from list_leads or create_lead."
          }
        },
        "required": [
          "leadId"
        ],
        "title": "get_lead"
      },
      "PortalOpsInput_get_production_rate": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "productionRates doc id."
          },
          "rateId": {
            "type": "string",
            "description": "Alias for id."
          }
        },
        "title": "get_production_rate"
      },
      "PortalOpsInput_get_settings": {
        "type": "object",
        "properties": {},
        "title": "get_settings"
      },
      "PortalOpsInput_ingest_receipt": {
        "type": "object",
        "properties": {
          "vendor": {
            "type": "string",
            "description": "Vendor name, e.g. Sherwin-Williams, Lowe's, Home Depot, Amazon. Others are kept as written."
          },
          "invoiceNumber": {
            "type": "string",
            "description": "Invoice / order / receipt number as printed. Omit only when the receipt has none."
          },
          "invoiceDate": {
            "type": "string",
            "description": "Purchase date, YYYY-MM-DD. Required when there is no invoiceNumber."
          },
          "amountUsd": {
            "type": "number",
            "description": "Receipt total in dollars, tax included."
          },
          "taxUsd": {
            "type": "number",
            "description": "Tax portion of the total, if shown."
          },
          "storeLocation": {
            "type": "string",
            "description": "Store name / number / town."
          },
          "po": {
            "type": "string",
            "description": "PO or job field on the receipt (crews type the job number or customer name). SHOP / STOCK means no job."
          },
          "jobNameHint": {
            "type": "string",
            "description": "Job or customer name written on the receipt."
          },
          "addressHint": {
            "type": "string",
            "description": "Delivery / ship-to address."
          },
          "receivedBy": {
            "type": "string",
            "description": "Who picked it up or the card holder."
          },
          "lineItems": {
            "type": "array",
            "description": "Up to 40 lines.",
            "items": {
              "type": "object",
              "properties": {
                "description": {
                  "type": "string",
                  "description": "Field `description` — see tool handler and Estimate Builder parity."
                },
                "amountUsd": {
                  "type": "number",
                  "description": "Field `amountUsd` — see tool handler and Estimate Builder parity."
                }
              }
            }
          },
          "fileBase64": {
            "type": "string",
            "description": "The receipt file, base64: PDF, JPG or PNG."
          },
          "fileName": {
            "type": "string",
            "description": "File name, e.g. sherwin-12345678901234.pdf."
          },
          "sourceMessageId": {
            "type": "string",
            "description": "Email Message-ID the receipt came from. Kept for the audit trail; not part of the duplicate key."
          },
          "sourceInbox": {
            "type": "string",
            "description": "Mailbox it was found in."
          },
          "noJob": {
            "type": "boolean",
            "description": "True for a shop / office / stock purchase that belongs to no job (posts to overhead). Ignored when a job clearly fits."
          }
        },
        "required": [
          "vendor",
          "amountUsd",
          "fileBase64"
        ],
        "title": "ingest_receipt"
      },
      "PortalOpsInput_link_recording": {
        "type": "object",
        "properties": {
          "estimateId": {
            "type": "string",
            "description": "Estimate linked to this job; alternative to jobId/jobRef."
          },
          "jobRef": {
            "type": "string",
            "description": "Field `jobRef` — see tool handler and Estimate Builder parity."
          },
          "jobId": {
            "type": "string",
            "description": "Portal job id."
          },
          "source": {
            "type": "string",
            "enum": [
              "plaud",
              "granola",
              "other"
            ],
            "description": "Field `source` — see tool handler and Estimate Builder parity."
          },
          "recordingId": {
            "type": "string",
            "description": "Field `recordingId` — see tool handler and Estimate Builder parity."
          },
          "title": {
            "type": "string",
            "description": "Field `title` — see tool handler and Estimate Builder parity."
          },
          "startedAt": {
            "type": "number",
            "description": "Epoch ms when the recording started."
          },
          "url": {
            "type": "string",
            "description": "Field `url` — see tool handler and Estimate Builder parity."
          },
          "dryRun": {
            "type": "boolean",
            "description": "When true, resolve job + validate recording but do not write."
          },
          "otherNote": {
            "type": "string",
            "description": "Required when source is other: why this recording id cannot be verified (min 8 chars)."
          }
        },
        "required": [
          "source",
          "recordingId"
        ],
        "title": "link_recording"
      },
      "PortalOpsInput_list_activity": {
        "type": "object",
        "properties": {
          "limit": {
            "type": "integer",
            "description": "Page size (default 50, max 200)."
          },
          "cursor": {
            "type": "string",
            "description": "Opaque nextCursor from the previous page."
          },
          "sinceMs": {
            "type": "number",
            "description": "Only records on/after this epoch ms (or pass `since` as ISO date)."
          },
          "untilMs": {
            "type": "number",
            "description": "Only records on/before this epoch ms (or pass `until` as ISO date)."
          },
          "since": {
            "type": "string",
            "description": "ISO date/time alternative to sinceMs."
          },
          "until": {
            "type": "string",
            "description": "ISO date/time alternative to untilMs."
          },
          "jobId": {
            "type": "string",
            "description": "Filter to one job."
          },
          "contactId": {
            "type": "string",
            "description": "Filter to one customer contact."
          },
          "customer": {
            "type": "string",
            "description": "Customer name / email / phone text match."
          }
        },
        "title": "list_activity"
      },
      "PortalOpsInput_list_app_import_batches": {
        "type": "object",
        "properties": {
          "limit": {
            "type": "number",
            "description": "Max batches. Default 20, max 50."
          }
        },
        "title": "list_app_import_batches"
      },
      "PortalOpsInput_list_appointments": {
        "type": "object",
        "properties": {
          "estimatorUserId": {
            "type": "string",
            "description": "Optional — only visits for this estimator uid."
          },
          "daysAhead": {
            "type": "number",
            "description": "Horizon from now when untilMs omitted (default 21, max 90)."
          },
          "sinceMs": {
            "type": "number",
            "description": "Optional epoch ms lower bound (default now-1d)."
          },
          "untilMs": {
            "type": "number",
            "description": "Optional epoch ms upper bound."
          },
          "includePending": {
            "type": "boolean",
            "description": "Include job.pendingEstimateVisit rows (default true)."
          },
          "includeCancelled": {
            "type": "boolean",
            "description": "Include cancelled visits (default false)."
          }
        },
        "required": [],
        "title": "list_appointments"
      },
      "PortalOpsInput_list_bank_inbox": {
        "type": "object",
        "properties": {
          "limit": {
            "type": "number",
            "description": "Max rows to return. Default 40, max 200."
          }
        },
        "title": "list_bank_inbox"
      },
      "PortalOpsInput_list_cfo_packs": {
        "type": "object",
        "properties": {},
        "title": "list_cfo_packs"
      },
      "PortalOpsInput_list_close_checklist": {
        "type": "object",
        "properties": {
          "limit": {
            "type": "number",
            "description": "Max checklist rows. Default 40."
          }
        },
        "title": "list_close_checklist"
      },
      "PortalOpsInput_list_contacts": {
        "type": "object",
        "properties": {
          "limit": {
            "type": "integer",
            "description": "Page size (default 50, max 200)."
          },
          "cursor": {
            "type": "string",
            "description": "Opaque nextCursor from the previous page."
          },
          "sinceMs": {
            "type": "number",
            "description": "Only records on/after this epoch ms (or pass `since` as ISO date)."
          },
          "untilMs": {
            "type": "number",
            "description": "Only records on/before this epoch ms (or pass `until` as ISO date)."
          },
          "since": {
            "type": "string",
            "description": "ISO date/time alternative to sinceMs."
          },
          "until": {
            "type": "string",
            "description": "ISO date/time alternative to untilMs."
          },
          "jobId": {
            "type": "string",
            "description": "Filter to one job."
          },
          "contactId": {
            "type": "string",
            "description": "Filter to one customer contact."
          },
          "customer": {
            "type": "string",
            "description": "Customer name / email / phone text match."
          }
        },
        "title": "list_contacts"
      },
      "PortalOpsInput_list_crew": {
        "type": "object",
        "properties": {},
        "title": "list_crew"
      },
      "PortalOpsInput_list_drip_sequences": {
        "type": "object",
        "properties": {},
        "title": "list_drip_sequences"
      },
      "PortalOpsInput_list_email_templates": {
        "type": "object",
        "properties": {
          "kind": {
            "type": "string",
            "description": "Optional single template key to focus on (e.g. estimateCustomer)."
          }
        },
        "title": "list_email_templates"
      },
      "PortalOpsInput_list_estimate_descriptors": {
        "type": "object",
        "properties": {
          "query": {
            "type": "string",
            "description": "Optional search across name/description/id."
          },
          "limit": {
            "type": "number",
            "description": "Max rows (default 80, max 120)."
          },
          "estimateId": {
            "type": "string",
            "description": "Return only the lines on this estimate (id / lineItemId / descriptorId / name / description / amount / hours / isOption) — one doc read, response scope:\"estimate\". Omit to list the org descriptor library."
          }
        },
        "title": "list_estimate_descriptors"
      },
      "PortalOpsInput_list_estimate_products": {
        "type": "object",
        "properties": {
          "query": {
            "type": "string",
            "description": "Optional search across name/group/finish/id."
          },
          "group": {
            "type": "string",
            "description": "Optional exact group filter."
          },
          "limit": {
            "type": "number",
            "description": "Max rows (default 100, max 150)."
          }
        },
        "title": "list_estimate_products"
      },
      "PortalOpsInput_list_estimates": {
        "type": "object",
        "properties": {
          "limit": {
            "type": "integer",
            "description": "Page size (default 50, max 200)."
          },
          "cursor": {
            "type": "string",
            "description": "Opaque nextCursor from the previous page."
          },
          "sinceMs": {
            "type": "number",
            "description": "Only records on/after this epoch ms (or pass `since` as ISO date)."
          },
          "untilMs": {
            "type": "number",
            "description": "Only records on/before this epoch ms (or pass `until` as ISO date)."
          },
          "since": {
            "type": "string",
            "description": "ISO date/time alternative to sinceMs."
          },
          "until": {
            "type": "string",
            "description": "ISO date/time alternative to untilMs."
          },
          "jobId": {
            "type": "string",
            "description": "Filter to one job."
          },
          "contactId": {
            "type": "string",
            "description": "Filter to one customer contact."
          },
          "customer": {
            "type": "string",
            "description": "Customer name / email / phone text match."
          },
          "status": {
            "type": "string",
            "description": "draft | scheduled | sent | accepted | declined."
          }
        },
        "title": "list_estimates"
      },
      "PortalOpsInput_list_followups": {
        "type": "object",
        "properties": {
          "limit": {
            "type": "integer",
            "description": "Page size (default 50, max 200)."
          },
          "cursor": {
            "type": "string",
            "description": "Opaque nextCursor from the previous page."
          },
          "sinceMs": {
            "type": "number",
            "description": "Only records on/after this epoch ms (or pass `since` as ISO date)."
          },
          "untilMs": {
            "type": "number",
            "description": "Only records on/before this epoch ms (or pass `until` as ISO date)."
          },
          "since": {
            "type": "string",
            "description": "ISO date/time alternative to sinceMs."
          },
          "until": {
            "type": "string",
            "description": "ISO date/time alternative to untilMs."
          },
          "jobId": {
            "type": "string",
            "description": "Filter to one job."
          },
          "contactId": {
            "type": "string",
            "description": "Filter to one customer contact."
          },
          "customer": {
            "type": "string",
            "description": "Customer name / email / phone text match."
          }
        },
        "title": "list_followups"
      },
      "PortalOpsInput_list_hours": {
        "type": "object",
        "properties": {
          "limit": {
            "type": "integer",
            "description": "Page size (default 50, max 200)."
          },
          "cursor": {
            "type": "string",
            "description": "Opaque nextCursor from the previous page."
          },
          "sinceMs": {
            "type": "number",
            "description": "Only records on/after this epoch ms (or pass `since` as ISO date)."
          },
          "untilMs": {
            "type": "number",
            "description": "Only records on/before this epoch ms (or pass `until` as ISO date)."
          },
          "since": {
            "type": "string",
            "description": "ISO date/time alternative to sinceMs."
          },
          "until": {
            "type": "string",
            "description": "ISO date/time alternative to untilMs."
          },
          "jobId": {
            "type": "string",
            "description": "Filter to one job."
          },
          "contactId": {
            "type": "string",
            "description": "Filter to one customer contact."
          },
          "customer": {
            "type": "string",
            "description": "Customer name / email / phone text match."
          },
          "userId": {
            "type": "string",
            "description": "Only this staff member."
          }
        },
        "title": "list_hours"
      },
      "PortalOpsInput_list_invoices": {
        "type": "object",
        "properties": {
          "limit": {
            "type": "integer",
            "description": "Page size (default 50, max 200)."
          },
          "cursor": {
            "type": "string",
            "description": "Opaque nextCursor from the previous page."
          },
          "sinceMs": {
            "type": "number",
            "description": "Only records on/after this epoch ms (or pass `since` as ISO date)."
          },
          "untilMs": {
            "type": "number",
            "description": "Only records on/before this epoch ms (or pass `until` as ISO date)."
          },
          "since": {
            "type": "string",
            "description": "ISO date/time alternative to sinceMs."
          },
          "until": {
            "type": "string",
            "description": "ISO date/time alternative to untilMs."
          },
          "jobId": {
            "type": "string",
            "description": "Filter to one job."
          },
          "contactId": {
            "type": "string",
            "description": "Filter to one customer contact."
          },
          "customer": {
            "type": "string",
            "description": "Customer name / email / phone text match."
          },
          "status": {
            "type": "string",
            "description": "draft | sent | partial | paid | void."
          }
        },
        "title": "list_invoices"
      },
      "PortalOpsInput_list_job_colors": {
        "type": "object",
        "properties": {
          "limit": {
            "type": "integer",
            "description": "Page size (default 50, max 200)."
          },
          "cursor": {
            "type": "string",
            "description": "Opaque nextCursor from the previous page."
          },
          "sinceMs": {
            "type": "number",
            "description": "Only records on/after this epoch ms (or pass `since` as ISO date)."
          },
          "untilMs": {
            "type": "number",
            "description": "Only records on/before this epoch ms (or pass `until` as ISO date)."
          },
          "since": {
            "type": "string",
            "description": "ISO date/time alternative to sinceMs."
          },
          "until": {
            "type": "string",
            "description": "ISO date/time alternative to untilMs."
          },
          "jobId": {
            "type": "string",
            "description": "Filter to one job."
          },
          "contactId": {
            "type": "string",
            "description": "Filter to one customer contact."
          },
          "customer": {
            "type": "string",
            "description": "Customer name / email / phone text match."
          }
        },
        "title": "list_job_colors"
      },
      "PortalOpsInput_list_job_costs": {
        "type": "object",
        "properties": {
          "limit": {
            "type": "integer",
            "description": "Page size (default 50, max 200)."
          },
          "cursor": {
            "type": "string",
            "description": "Opaque nextCursor from the previous page."
          },
          "sinceMs": {
            "type": "number",
            "description": "Only records on/after this epoch ms (or pass `since` as ISO date)."
          },
          "untilMs": {
            "type": "number",
            "description": "Only records on/before this epoch ms (or pass `until` as ISO date)."
          },
          "since": {
            "type": "string",
            "description": "ISO date/time alternative to sinceMs."
          },
          "until": {
            "type": "string",
            "description": "ISO date/time alternative to untilMs."
          },
          "jobId": {
            "type": "string",
            "description": "Filter to one job."
          },
          "contactId": {
            "type": "string",
            "description": "Filter to one customer contact."
          },
          "customer": {
            "type": "string",
            "description": "Customer name / email / phone text match."
          }
        },
        "title": "list_job_costs"
      },
      "PortalOpsInput_list_jobs": {
        "type": "object",
        "properties": {
          "view": {
            "type": "string",
            "enum": [
              "summary",
              "scope",
              "full"
            ],
            "description": "Lean read shape. Default summary always includes writeVersion + totals."
          },
          "fields": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Optional whitelist of top-level response keys."
          },
          "compact": {
            "type": "boolean",
            "description": "For large lists: gzip JSON as rowsGzipBase64 / jobsGzipBase64 (threshold ~40 rows)."
          }
        },
        "title": "list_jobs"
      },
      "PortalOpsInput_list_leads": {
        "type": "object",
        "properties": {
          "limit": {
            "type": "integer",
            "description": "Page size (default 50, max 200)."
          },
          "cursor": {
            "type": "string",
            "description": "Opaque nextCursor from the previous page."
          },
          "sinceMs": {
            "type": "number",
            "description": "Only records on/after this epoch ms (or pass `since` as ISO date)."
          },
          "untilMs": {
            "type": "number",
            "description": "Only records on/before this epoch ms (or pass `until` as ISO date)."
          },
          "since": {
            "type": "string",
            "description": "ISO date/time alternative to sinceMs."
          },
          "until": {
            "type": "string",
            "description": "ISO date/time alternative to untilMs."
          },
          "jobId": {
            "type": "string",
            "description": "Filter to one job."
          },
          "contactId": {
            "type": "string",
            "description": "Filter to one customer contact."
          },
          "customer": {
            "type": "string",
            "description": "Customer name / email / phone text match."
          },
          "status": {
            "type": "string",
            "description": "Optional status id: new, contacted, estimate_scheduled, won, lost."
          },
          "includeDeleted": {
            "type": "boolean",
            "description": "Include leads marked deleted: true."
          }
        },
        "title": "list_leads"
      },
      "PortalOpsInput_list_learning_ai": {
        "type": "object",
        "properties": {},
        "title": "list_learning_ai"
      },
      "PortalOpsInput_list_messages": {
        "type": "object",
        "properties": {
          "limit": {
            "type": "integer",
            "description": "Page size (default 50, max 200)."
          },
          "cursor": {
            "type": "string",
            "description": "Opaque nextCursor from the previous page."
          },
          "sinceMs": {
            "type": "number",
            "description": "Only records on/after this epoch ms (or pass `since` as ISO date)."
          },
          "untilMs": {
            "type": "number",
            "description": "Only records on/before this epoch ms (or pass `until` as ISO date)."
          },
          "since": {
            "type": "string",
            "description": "ISO date/time alternative to sinceMs."
          },
          "until": {
            "type": "string",
            "description": "ISO date/time alternative to untilMs."
          },
          "jobId": {
            "type": "string",
            "description": "Filter to one job."
          },
          "contactId": {
            "type": "string",
            "description": "Filter to one customer contact."
          },
          "customer": {
            "type": "string",
            "description": "Customer name / email / phone text match."
          }
        },
        "title": "list_messages"
      },
      "PortalOpsInput_list_payments": {
        "type": "object",
        "properties": {
          "limit": {
            "type": "integer",
            "description": "Page size (default 50, max 200)."
          },
          "cursor": {
            "type": "string",
            "description": "Opaque nextCursor from the previous page."
          },
          "sinceMs": {
            "type": "number",
            "description": "Only records on/after this epoch ms (or pass `since` as ISO date)."
          },
          "untilMs": {
            "type": "number",
            "description": "Only records on/before this epoch ms (or pass `until` as ISO date)."
          },
          "since": {
            "type": "string",
            "description": "ISO date/time alternative to sinceMs."
          },
          "until": {
            "type": "string",
            "description": "ISO date/time alternative to untilMs."
          },
          "jobId": {
            "type": "string",
            "description": "Filter to one job."
          },
          "contactId": {
            "type": "string",
            "description": "Filter to one customer contact."
          },
          "customer": {
            "type": "string",
            "description": "Customer name / email / phone text match."
          }
        },
        "title": "list_payments"
      },
      "PortalOpsInput_list_pending_checkins": {
        "type": "object",
        "properties": {
          "jobId": {
            "type": "string",
            "description": "Optional jobId to filter pending check-ins to one job."
          }
        },
        "title": "list_pending_checkins"
      },
      "PortalOpsInput_list_pending_share_photos": {
        "type": "object",
        "properties": {
          "limit": {
            "type": "integer",
            "description": "Page size (default 50, max 200)."
          },
          "cursor": {
            "type": "string",
            "description": "Opaque nextCursor from the previous page."
          },
          "sinceMs": {
            "type": "number",
            "description": "Only records on/after this epoch ms (or pass `since` as ISO date)."
          },
          "untilMs": {
            "type": "number",
            "description": "Only records on/before this epoch ms (or pass `until` as ISO date)."
          },
          "since": {
            "type": "string",
            "description": "ISO date/time alternative to sinceMs."
          },
          "until": {
            "type": "string",
            "description": "ISO date/time alternative to untilMs."
          },
          "jobId": {
            "type": "string",
            "description": "Filter to one job."
          },
          "contactId": {
            "type": "string",
            "description": "Filter to one customer contact."
          },
          "customer": {
            "type": "string",
            "description": "Customer name / email / phone text match."
          },
          "estimateId": {
            "type": "string",
            "description": "Estimate document id."
          }
        },
        "title": "list_pending_share_photos"
      },
      "PortalOpsInput_list_photos": {
        "type": "object",
        "properties": {
          "limit": {
            "type": "integer",
            "description": "Page size (default 50, max 200)."
          },
          "cursor": {
            "type": "string",
            "description": "Opaque nextCursor from the previous page."
          },
          "sinceMs": {
            "type": "number",
            "description": "Only records on/after this epoch ms (or pass `since` as ISO date)."
          },
          "untilMs": {
            "type": "number",
            "description": "Only records on/before this epoch ms (or pass `until` as ISO date)."
          },
          "since": {
            "type": "string",
            "description": "ISO date/time alternative to sinceMs."
          },
          "until": {
            "type": "string",
            "description": "ISO date/time alternative to untilMs."
          },
          "jobId": {
            "type": "string",
            "description": "Filter to one job."
          },
          "contactId": {
            "type": "string",
            "description": "Filter to one customer contact."
          },
          "customer": {
            "type": "string",
            "description": "Customer name / email / phone text match."
          },
          "scope": {
            "type": "string",
            "enum": [
              "job",
              "estimate",
              "checkin",
              "assistant"
            ],
            "description": "Field `scope` — see tool handler and Estimate Builder parity."
          },
          "estimateId": {
            "type": "string",
            "description": "Estimate document id."
          },
          "includeNonCustomerVisible": {
            "type": "boolean",
            "description": "Field `includeNonCustomerVisible` — see tool handler and Estimate Builder parity."
          }
        },
        "title": "list_photos"
      },
      "PortalOpsInput_list_production": {
        "type": "object",
        "properties": {
          "limit": {
            "type": "integer",
            "description": "Page size (default 50, max 200)."
          },
          "cursor": {
            "type": "string",
            "description": "Opaque nextCursor from the previous page."
          },
          "sinceMs": {
            "type": "number",
            "description": "Only records on/after this epoch ms (or pass `since` as ISO date)."
          },
          "untilMs": {
            "type": "number",
            "description": "Only records on/before this epoch ms (or pass `until` as ISO date)."
          },
          "since": {
            "type": "string",
            "description": "ISO date/time alternative to sinceMs."
          },
          "until": {
            "type": "string",
            "description": "ISO date/time alternative to untilMs."
          },
          "jobId": {
            "type": "string",
            "description": "Filter to one job."
          },
          "contactId": {
            "type": "string",
            "description": "Filter to one customer contact."
          },
          "customer": {
            "type": "string",
            "description": "Customer name / email / phone text match."
          },
          "status": {
            "type": "string",
            "description": "estimate | accepted | active | limbo | completed | archived."
          }
        },
        "title": "list_production"
      },
      "PortalOpsInput_list_production_rates": {
        "type": "object",
        "properties": {
          "query": {
            "type": "string",
            "description": "Optional search across name/group/clientLabel/id."
          },
          "group": {
            "type": "string",
            "description": "Optional exact group filter, e.g. Interior, Ceilings."
          },
          "limit": {
            "type": "number",
            "description": "Max rows to return (default 100, max 150)."
          }
        },
        "title": "list_production_rates"
      },
      "PortalOpsInput_list_proposals": {
        "type": "object",
        "properties": {
          "view": {
            "type": "string",
            "enum": [
              "summary",
              "scope",
              "full"
            ],
            "description": "Lean read shape. Default summary always includes writeVersion + totals."
          },
          "fields": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Optional whitelist of top-level response keys."
          },
          "compact": {
            "type": "boolean",
            "description": "For large lists: gzip JSON as rowsGzipBase64 / jobsGzipBase64 (threshold ~40 rows)."
          },
          "filter": {
            "type": "string",
            "description": "all | new | draft | sent | accepted | declined. Default all."
          },
          "search": {
            "type": "string",
            "description": "Optional name/address/email search."
          }
        },
        "title": "list_proposals"
      },
      "PortalOpsInput_list_receipts": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "description": "pending | needs_review | proposed | uploaded | confirmed | dismissed | all. Default pending."
          },
          "limit": {
            "type": "number",
            "description": "Max rows. Default 50, max 200."
          }
        },
        "title": "list_receipts"
      },
      "PortalOpsInput_list_recordings": {
        "type": "object",
        "properties": {
          "source": {
            "type": "string",
            "enum": [
              "plaud",
              "granola"
            ],
            "description": "Field `source` — see tool handler and Estimate Builder parity."
          },
          "page": {
            "type": "integer",
            "minimum": 1,
            "description": "Plaud page, starting at 1."
          },
          "cursor": {
            "type": "string",
            "description": "Granola cursor returned by the previous page."
          }
        },
        "required": [
          "source"
        ],
        "title": "list_recordings"
      },
      "PortalOpsInput_list_recurring_reminders": {
        "type": "object",
        "properties": {},
        "title": "list_recurring_reminders"
      },
      "PortalOpsInput_list_reviews_referrals": {
        "type": "object",
        "properties": {
          "limit": {
            "type": "integer",
            "description": "Page size (default 50, max 200)."
          },
          "cursor": {
            "type": "string",
            "description": "Opaque nextCursor from the previous page."
          },
          "sinceMs": {
            "type": "number",
            "description": "Only records on/after this epoch ms (or pass `since` as ISO date)."
          },
          "untilMs": {
            "type": "number",
            "description": "Only records on/before this epoch ms (or pass `until` as ISO date)."
          },
          "since": {
            "type": "string",
            "description": "ISO date/time alternative to sinceMs."
          },
          "until": {
            "type": "string",
            "description": "ISO date/time alternative to untilMs."
          },
          "jobId": {
            "type": "string",
            "description": "Filter to one job."
          },
          "contactId": {
            "type": "string",
            "description": "Filter to one customer contact."
          },
          "customer": {
            "type": "string",
            "description": "Customer name / email / phone text match."
          }
        },
        "title": "list_reviews_referrals"
      },
      "PortalOpsInput_list_sales_leads": {
        "type": "object",
        "properties": {
          "search": {
            "type": "string",
            "description": "Optional name/address/email search."
          }
        },
        "title": "list_sales_leads"
      },
      "PortalOpsInput_list_schedule": {
        "type": "object",
        "properties": {
          "limit": {
            "type": "integer",
            "description": "Page size (default 50, max 200)."
          },
          "cursor": {
            "type": "string",
            "description": "Opaque nextCursor from the previous page."
          },
          "sinceMs": {
            "type": "number",
            "description": "Only records on/after this epoch ms (or pass `since` as ISO date)."
          },
          "untilMs": {
            "type": "number",
            "description": "Only records on/before this epoch ms (or pass `until` as ISO date)."
          },
          "since": {
            "type": "string",
            "description": "ISO date/time alternative to sinceMs."
          },
          "until": {
            "type": "string",
            "description": "ISO date/time alternative to untilMs."
          },
          "jobId": {
            "type": "string",
            "description": "Filter to one job."
          },
          "contactId": {
            "type": "string",
            "description": "Filter to one customer contact."
          },
          "customer": {
            "type": "string",
            "description": "Customer name / email / phone text match."
          },
          "days": {
            "type": "integer",
            "description": "Days ahead for open slots (default 14, max 60)."
          },
          "estimatorUserId": {
            "type": "string",
            "description": "Only this estimator."
          }
        },
        "title": "list_schedule"
      },
      "PortalOpsInput_list_staff": {
        "type": "object",
        "properties": {
          "limit": {
            "type": "integer",
            "description": "Page size (default 50, max 200)."
          },
          "cursor": {
            "type": "string",
            "description": "Opaque nextCursor from the previous page."
          },
          "sinceMs": {
            "type": "number",
            "description": "Only records on/after this epoch ms (or pass `since` as ISO date)."
          },
          "untilMs": {
            "type": "number",
            "description": "Only records on/before this epoch ms (or pass `until` as ISO date)."
          },
          "since": {
            "type": "string",
            "description": "ISO date/time alternative to sinceMs."
          },
          "until": {
            "type": "string",
            "description": "ISO date/time alternative to untilMs."
          },
          "jobId": {
            "type": "string",
            "description": "Filter to one job."
          },
          "contactId": {
            "type": "string",
            "description": "Filter to one customer contact."
          },
          "customer": {
            "type": "string",
            "description": "Customer name / email / phone text match."
          },
          "role": {
            "type": "string",
            "description": "Optional role filter (admin, lead, crew, estimator, office)."
          }
        },
        "title": "list_staff"
      },
      "PortalOpsInput_list_teams": {
        "type": "object",
        "properties": {},
        "title": "list_teams"
      },
      "PortalOpsInput_list_tools": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "all",
              "available",
              "at_job",
              "needs_repair"
            ],
            "description": "Optional status filter. Default 'all'."
          }
        },
        "title": "list_tools"
      },
      "PortalOpsInput_list_webhooks": {
        "type": "object",
        "properties": {},
        "title": "list_webhooks"
      },
      "PortalOpsInput_mark_tool_fixed": {
        "type": "object",
        "properties": {
          "toolId": {
            "type": "string",
            "description": "Field `toolId` — see tool handler and Estimate Builder parity."
          },
          "byName": {
            "type": "string",
            "description": "Field `byName` — see tool handler and Estimate Builder parity."
          },
          "note": {
            "type": "string",
            "description": "Field `note` — see tool handler and Estimate Builder parity."
          }
        },
        "required": [
          "toolId"
        ],
        "title": "mark_tool_fixed"
      },
      "PortalOpsInput_mark_tool_repair": {
        "type": "object",
        "properties": {
          "toolId": {
            "type": "string",
            "description": "Field `toolId` — see tool handler and Estimate Builder parity."
          },
          "byName": {
            "type": "string",
            "description": "Field `byName` — see tool handler and Estimate Builder parity."
          },
          "note": {
            "type": "string",
            "description": "What's wrong."
          }
        },
        "required": [
          "toolId"
        ],
        "title": "mark_tool_repair"
      },
      "PortalOpsInput_match_bank_line": {
        "type": "object",
        "properties": {
          "txnId": {
            "type": "string",
            "description": "Bank transaction id."
          },
          "jobId": {
            "type": "string",
            "description": "Optional job id to match."
          },
          "invoiceId": {
            "type": "string",
            "description": "Optional invoice id to match."
          },
          "category": {
            "type": "string",
            "description": "Optional expense category code."
          }
        },
        "required": [
          "txnId"
        ],
        "title": "match_bank_line"
      },
      "PortalOpsInput_offer_estimate_visit": {
        "type": "object",
        "properties": {
          "jobId": {
            "type": "string",
            "description": "Portal job id."
          },
          "jobName": {
            "type": "string",
            "description": "Field `jobName` — see tool handler and Estimate Builder parity."
          },
          "startAt": {
            "type": "number",
            "description": "Field `startAt` — see tool handler and Estimate Builder parity."
          },
          "endAt": {
            "type": "number",
            "description": "Field `endAt` — see tool handler and Estimate Builder parity."
          },
          "estimatorUserId": {
            "type": "string",
            "description": "Field `estimatorUserId` — see tool handler and Estimate Builder parity."
          },
          "estimatorName": {
            "type": "string",
            "description": "Field `estimatorName` — see tool handler and Estimate Builder parity."
          },
          "notes": {
            "type": "string",
            "description": "Field `notes` — see tool handler and Estimate Builder parity."
          }
        },
        "required": [
          "startAt"
        ],
        "title": "offer_estimate_visit"
      },
      "PortalOpsInput_polish_delay_reason": {
        "type": "object",
        "properties": {
          "jobId": {
            "type": "string",
            "description": "Portal job id."
          },
          "jobName": {
            "type": "string",
            "description": "Field `jobName` — see tool handler and Estimate Builder parity."
          },
          "kind": {
            "type": "string",
            "description": "rain | heat | other"
          },
          "reason": {
            "type": "string",
            "description": "Draft crew delay note."
          }
        },
        "required": [
          "reason"
        ],
        "title": "polish_delay_reason"
      },
      "PortalOpsInput_polish_limbo_reason": {
        "type": "object",
        "properties": {
          "jobId": {
            "type": "string",
            "description": "Portal job id."
          },
          "jobName": {
            "type": "string",
            "description": "Field `jobName` — see tool handler and Estimate Builder parity."
          },
          "reason": {
            "type": "string",
            "description": "Draft staff note to polish."
          }
        },
        "required": [
          "reason"
        ],
        "title": "polish_limbo_reason"
      },
      "PortalOpsInput_post_job_delay": {
        "type": "object",
        "properties": {
          "jobId": {
            "type": "string",
            "description": "Portal job id."
          },
          "jobName": {
            "type": "string",
            "description": "Field `jobName` — see tool handler and Estimate Builder parity."
          },
          "newEndDate": {
            "type": "string",
            "description": "Optional explicit finish YYYY-MM-DD (otherwise +1 workday from current endDate)."
          },
          "delayKind": {
            "type": "string",
            "description": "rain | heat | other (other = this job only, no cascade unless cascade:true)."
          },
          "reason": {
            "type": "string",
            "description": "Optional customer-facing note (polished delay copy)."
          },
          "cascade": {
            "type": "boolean",
            "description": "true = force crew cascade; false = never cascade; omit = rain/heat cascade, other does not."
          },
          "date": {
            "type": "string",
            "description": "Optional delay day YYYY-MM-DD (defaults to today in org timezone handling)."
          }
        },
        "required": [],
        "title": "post_job_delay"
      },
      "PortalOpsInput_preview_app_import": {
        "type": "object",
        "properties": {
          "source": {
            "type": "string",
            "enum": [
              "paintscout",
              "generic"
            ],
            "description": "paintscout or generic. dripjobs returns coming-soon. Anything else is bad-source."
          },
          "mode": {
            "type": "string",
            "enum": [
              "unresolved-jobs",
              "sales-history"
            ],
            "description": "unresolved-jobs (default) or sales-history. Anything else is bad-mode."
          },
          "tarGzBase64": {
            "type": "string",
            "description": "PaintScout export .tar.gz, base64. Decoded size counts toward the 4 MB cap."
          },
          "files": {
            "type": "object",
            "description": "PaintScout loose files instead of tarGzBase64: { \"raw/quotes_full.json\": \"<json text>\", \"contacts.csv\": \"<csv text>\", … }. At most 200 files.",
            "additionalProperties": {
              "type": "string"
            }
          },
          "csv": {
            "type": "string",
            "description": "Spreadsheet template CSV text (source generic)."
          },
          "quoteNumbers": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Optional: only these quote numbers."
          },
          "recordIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Optional: only these record ids from a prior preview's rows."
          },
          "query": {
            "type": "string",
            "description": "Optional text filter (quote number, customer, address, status)."
          },
          "status": {
            "type": "string",
            "description": "Optional exact status filter."
          },
          "maxRows": {
            "type": "number",
            "description": "Sample rows / actions / duplicates returned. Default 50, max 200. Counts are always complete."
          }
        },
        "required": [
          "source"
        ],
        "title": "preview_app_import"
      },
      "PortalOpsInput_publish_checkins": {
        "type": "object",
        "properties": {
          "checkinIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Specific pending check-in IDs from the PENDING CHECK-INS block or list_pending_checkins result."
          },
          "jobId": {
            "type": "string",
            "description": "Optional jobId; publishes all pending check-ins on that job when checkinIds is omitted."
          },
          "allPending": {
            "type": "boolean",
            "description": "Set true only when the admin clearly says all pending check-ins."
          },
          "reason": {
            "type": "string",
            "description": "Short audit note for the Confirm card, e.g. 'admin asked to publish pending check-ins'."
          }
        },
        "title": "publish_checkins"
      },
      "PortalOpsInput_query_cfo_knowledge": {
        "type": "object",
        "properties": {
          "packId": {
            "type": "string",
            "enum": [
              "month",
              "job_costing",
              "budget_variance",
              "job_type",
              "crew_costing",
              "hours_kpis",
              "sales_pulse",
              "ar_cash",
              "underpriced",
              "advisor_actions",
              "cash",
              "pl_actual",
              "bank_uncoded",
              "close_month"
            ],
            "description": "CFO pack to query. Default month."
          },
          "period": {
            "type": "string",
            "enum": [
              "this_month",
              "last_month"
            ],
            "description": "Reporting period. Default this_month."
          },
          "comparePeriod": {
            "type": "string",
            "enum": [
              "this_month",
              "last_month"
            ],
            "description": "Optional comparison period."
          },
          "jobId": {
            "type": "string",
            "description": "Optional job id filter for job-level packs."
          },
          "dimension": {
            "type": "string",
            "description": "Optional row dimension filter, such as job or type."
          },
          "filters": {
            "type": "object",
            "description": "Optional primitive or min/max filters by returned row field."
          },
          "sort": {
            "type": "string",
            "description": "Optional sort field, prefix with '-' for descending."
          },
          "limit": {
            "type": "number",
            "description": "Max rows to return. Default 40, max 200."
          },
          "offset": {
            "type": "number",
            "description": "Pagination offset. Default 0."
          }
        },
        "title": "query_cfo_knowledge"
      },
      "PortalOpsInput_read_estimate_handoff": {
        "type": "object",
        "properties": {
          "estimateId": {
            "type": "string",
            "description": "Estimate document id."
          },
          "jobId": {
            "type": "string",
            "description": "Portal job id."
          },
          "jobRef": {
            "type": "string",
            "description": "Field `jobRef` — see tool handler and Estimate Builder parity."
          }
        },
        "title": "read_estimate_handoff"
      },
      "PortalOpsInput_read_home_file": {
        "type": "object",
        "properties": {
          "contactId": {
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            ],
            "description": "One contact id, or an array of up to 20."
          },
          "jobId": {
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            ],
            "description": "One job id or job number (or an array). Resolves to the contact linked to that job."
          }
        },
        "required": [],
        "title": "read_home_file"
      },
      "PortalOpsInput_read_job": {
        "type": "object",
        "properties": {
          "view": {
            "type": "string",
            "enum": [
              "summary",
              "scope",
              "full"
            ],
            "description": "Lean read shape. Default summary always includes writeVersion + totals."
          },
          "fields": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Optional whitelist of top-level response keys."
          },
          "compact": {
            "type": "boolean",
            "description": "For large lists: gzip JSON as rowsGzipBase64 / jobsGzipBase64 (threshold ~40 rows)."
          },
          "jobId": {
            "type": "string",
            "description": "Portal job id."
          }
        },
        "required": [
          "jobId"
        ],
        "title": "read_job"
      },
      "PortalOpsInput_read_proposal": {
        "type": "object",
        "properties": {
          "view": {
            "type": "string",
            "enum": [
              "summary",
              "scope",
              "full"
            ],
            "description": "Lean read shape. Default summary always includes writeVersion + totals."
          },
          "fields": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Optional whitelist of top-level response keys."
          },
          "compact": {
            "type": "boolean",
            "description": "For large lists: gzip JSON as rowsGzipBase64 / jobsGzipBase64 (threshold ~40 rows)."
          },
          "estimateId": {
            "type": "string",
            "description": "Estimate document id."
          },
          "jobId": {
            "type": "string",
            "description": "Portal job id."
          }
        },
        "title": "read_proposal"
      },
      "PortalOpsInput_read_recording_transcript": {
        "type": "object",
        "properties": {
          "source": {
            "type": "string",
            "enum": [
              "plaud",
              "granola"
            ],
            "description": "Field `source` — see tool handler and Estimate Builder parity."
          },
          "recordingId": {
            "type": "string",
            "description": "Field `recordingId` — see tool handler and Estimate Builder parity."
          },
          "offset": {
            "type": "integer",
            "minimum": 0,
            "description": "Character offset; start at 0, then follow nextOffset."
          }
        },
        "required": [
          "source",
          "recordingId"
        ],
        "title": "read_recording_transcript"
      },
      "PortalOpsInput_record_collected_payment": {
        "type": "object",
        "properties": {
          "jobId": {
            "type": "string",
            "description": "Job id from list_jobs / CURRENT JOBS."
          },
          "kind": {
            "type": "string",
            "enum": [
              "deposit",
              "custom"
            ],
            "description": "deposit, or custom for any other payment. Default custom."
          },
          "amount": {
            "type": "number",
            "description": "Dollars collected (e.g. 1500 or 1500.50). Must be greater than 0."
          },
          "method": {
            "type": "string",
            "enum": [
              "check",
              "cash",
              "venmo",
              "zelle",
              "cash_app",
              "paypal",
              "other"
            ],
            "description": "How it was paid. check needs checkNumber; other needs note."
          },
          "date": {
            "type": "string",
            "description": "Date collected, YYYY-MM-DD. Not in the future."
          },
          "reference": {
            "type": "string",
            "description": "Optional reference (e.g. PaintScout payment number)."
          },
          "checkNumber": {
            "type": "string",
            "description": "Required when method is check."
          },
          "note": {
            "type": "string",
            "description": "Staff note. Required when method is other."
          },
          "confirmSeparate": {
            "type": "boolean",
            "description": "Only after the admin confirmed a needsConfirm duplicate question is a separate payment."
          }
        },
        "required": [
          "jobId",
          "amount",
          "method",
          "date"
        ],
        "title": "record_collected_payment"
      },
      "PortalOpsInput_remember": {
        "type": "object",
        "properties": {
          "fact": {
            "type": "string",
            "description": "One short declarative sentence, e.g. 'We use BM Advance Semi-Gloss for all interior trim'."
          }
        },
        "required": [
          "fact"
        ],
        "title": "remember"
      },
      "PortalOpsInput_remove_job_file": {
        "type": "object",
        "properties": {
          "jobId": {
            "type": "string",
            "description": "Existing job id (e.g. from list_jobs / read_job)."
          },
          "storagePath": {
            "type": "string",
            "description": "Preferred — full orgs/{orgId}/jobs/{jobId}/{ts}_{fileName} path, or a unique path fragment / timestamp prefix."
          },
          "path": {
            "type": "string",
            "description": "Alias for storagePath."
          },
          "filePath": {
            "type": "string",
            "description": "Alias for storagePath."
          },
          "fileName": {
            "type": "string",
            "description": "Exact file name on job.files[] (e.g. po-1349-marlene-vogler.pdf). Never a substring — TEXT WOs must not match raster names."
          },
          "name": {
            "type": "string",
            "description": "Alias for fileName."
          },
          "fileId": {
            "type": "string",
            "description": "Optional id or storage timestamp prefix (e.g. 1788595995663)."
          }
        },
        "required": [
          "jobId"
        ],
        "title": "remove_job_file"
      },
      "PortalOpsInput_remove_spot_assignment": {
        "type": "object",
        "properties": {
          "jobId": {
            "type": "string",
            "description": "Job ID from list_jobs."
          },
          "jobName": {
            "type": "string",
            "description": "Alternatively the job's name, exactly as in list_jobs."
          },
          "spotId": {
            "type": "string",
            "description": "The spot assignment id (shown in brackets in the CURRENT JOBS 'spot:' line). Preferred."
          },
          "userName": {
            "type": "string",
            "description": "Crew member's display name — used to find their spot when spotId isn't given."
          },
          "start": {
            "type": "string",
            "description": "Optional day (YYYY-MM-DD) to narrow which of that person's spots to remove."
          }
        },
        "required": [],
        "title": "remove_spot_assignment"
      },
      "PortalOpsInput_rename_job": {
        "type": "object",
        "properties": {
          "jobId": {
            "type": "string",
            "description": "Existing job id (e.g. from list_jobs / create_job)."
          },
          "name": {
            "type": "string",
            "description": "New job title — PO/quote number like '1253'. Never the customer name."
          },
          "jobNumber": {
            "type": "string",
            "description": "Alias for name — PO/quote number."
          },
          "poNumber": {
            "type": "string",
            "description": "Alias for name — PO/quote number."
          },
          "quoteNumber": {
            "type": "string",
            "description": "Alias for name — PO/quote number."
          },
          "customerName": {
            "type": "string",
            "description": "Optional contact field (e.g. 'Kim Brandt'). Does not become the job title."
          },
          "address": {
            "type": "string",
            "description": "Optional job site address update."
          },
          "phone": {
            "type": "string",
            "description": "Optional customer phone."
          },
          "customerPhone": {
            "type": "string",
            "description": "Optional customer phone."
          },
          "email": {
            "type": "string",
            "description": "Optional customer email."
          },
          "customerEmail": {
            "type": "string",
            "description": "Optional customer email."
          }
        },
        "required": [
          "jobId",
          "name"
        ],
        "title": "rename_job"
      },
      "PortalOpsInput_reopen_estimate_draft": {
        "type": "object",
        "properties": {
          "estimateId": {
            "type": "string",
            "description": "Estimate document id."
          },
          "jobId": {
            "type": "string",
            "description": "Portal job id."
          }
        },
        "title": "reopen_estimate_draft"
      },
      "PortalOpsInput_reopen_proposal_draft": {
        "type": "object",
        "properties": {
          "estimateId": {
            "type": "string",
            "description": "Estimate document id."
          },
          "jobId": {
            "type": "string",
            "description": "Portal job id."
          }
        },
        "title": "reopen_proposal_draft"
      },
      "PortalOpsInput_research_estimate_product": {
        "type": "object",
        "properties": {
          "query": {
            "type": "string",
            "description": "Product search phrase, e.g. WoodScapes Exterior Polyurethane Semi-Transparent House Stain."
          },
          "name": {
            "type": "string",
            "description": "Optional preferred catalog name."
          },
          "brand": {
            "type": "string",
            "description": "Field `brand` — see tool handler and Estimate Builder parity."
          },
          "line": {
            "type": "string",
            "description": "Field `line` — see tool handler and Estimate Builder parity."
          },
          "sheen": {
            "type": "string",
            "description": "Field `sheen` — see tool handler and Estimate Builder parity."
          },
          "finish": {
            "type": "string",
            "description": "Field `finish` — see tool handler and Estimate Builder parity."
          },
          "group": {
            "type": "string",
            "description": "e.g. Exterior Siding, Interior wall."
          },
          "pricePerUnit": {
            "type": "number",
            "description": "Optional $/gal the admin already stated."
          },
          "coveragePerUnit": {
            "type": "number",
            "description": "Optional sqft/gal the admin already stated."
          }
        },
        "required": [
          "query"
        ],
        "title": "research_estimate_product"
      },
      "PortalOpsInput_resend_proposal_email": {
        "type": "object",
        "properties": {
          "estimateId": {
            "type": "string",
            "description": "Sent estimate id (or pass jobId)."
          },
          "jobId": {
            "type": "string",
            "description": "Job id whose estimateId is the sent proposal."
          },
          "email": {
            "type": "string",
            "description": "One extra recipient email address."
          }
        },
        "required": [
          "email"
        ],
        "title": "resend_proposal_email"
      },
      "PortalOpsInput_restore_proposal_revision": {
        "type": "object",
        "properties": {
          "estimateId": {
            "type": "string",
            "description": "Draft estimate id."
          },
          "jobId": {
            "type": "string",
            "description": "Job with estimateId — alternative to estimateId."
          },
          "revisionId": {
            "type": "string",
            "description": "Revision doc id from estimate revisions subcollection."
          },
          "ifVersion": {
            "type": "integer",
            "description": "Optional writeVersion guard."
          },
          "dryRun": {
            "type": "boolean",
            "description": "Preview restore without persisting."
          },
          "idempotencyKey": {
            "type": "string",
            "description": "24h idempotent retry key."
          },
          "returnView": {
            "type": "string",
            "enum": [
              "summary",
              "scope",
              "full"
            ],
            "description": "Shape of updatedState on success (default scope)."
          }
        },
        "title": "restore_proposal_revision"
      },
      "PortalOpsInput_save_annotated_photo": {
        "type": "object",
        "properties": {
          "sourcePhotoId": {
            "type": "string",
            "description": "Assistant photo id (from upload_photo or list_photos)."
          },
          "jobId": {
            "type": "string",
            "description": "With sourceStoragePath / sourceUrl to annotate an existing job photo."
          },
          "sourceStoragePath": {
            "type": "string",
            "description": "Field `sourceStoragePath` — see tool handler and Estimate Builder parity."
          },
          "sourceUrl": {
            "type": "string",
            "description": "Field `sourceUrl` — see tool handler and Estimate Builder parity."
          },
          "imageBase64": {
            "type": "string",
            "description": "Annotated image bytes, base64."
          },
          "contentType": {
            "type": "string",
            "description": "Field `contentType` — see tool handler and Estimate Builder parity."
          },
          "caption": {
            "type": "string",
            "description": "Field `caption` — see tool handler and Estimate Builder parity."
          },
          "room": {
            "type": "string",
            "description": "Field `room` — see tool handler and Estimate Builder parity."
          },
          "annotationNotes": {
            "type": "string",
            "description": "What the markup shows (staff-only)."
          },
          "fileName": {
            "type": "string",
            "description": "Field `fileName` — see tool handler and Estimate Builder parity."
          }
        },
        "required": [
          "imageBase64"
        ],
        "title": "save_annotated_photo"
      },
      "PortalOpsInput_save_estimate_settings": {
        "type": "object",
        "properties": {
          "hourlyRate": {
            "type": "number",
            "description": "Billable labor $/hr on estimates."
          },
          "taxRate": {
            "type": "number",
            "description": "Tax percent."
          },
          "wastePercent": {
            "type": "number",
            "description": "Field `wastePercent` — see tool handler and Estimate Builder parity."
          },
          "showSubstratePriceDescriptors": {
            "type": "boolean",
            "description": "Field `showSubstratePriceDescriptors` — see tool handler and Estimate Builder parity."
          },
          "packagePricingEnabled": {
            "type": "boolean",
            "description": "Field `packagePricingEnabled` — see tool handler and Estimate Builder parity."
          },
          "defaultProductionRates": {
            "type": "object",
            "description": "Map of slot → production rate id (walls, ceiling, floor, trim, siding, door, window, cabinets)."
          },
          "slot": {
            "type": "string",
            "enum": [
              "walls",
              "ceiling",
              "floor",
              "trim",
              "siding",
              "door",
              "window",
              "cabinets"
            ],
            "description": "Set one default slot (with productionRateId)."
          },
          "productionRateId": {
            "type": "string",
            "description": "Rate id for slot. Empty clears."
          }
        },
        "title": "save_estimate_settings"
      },
      "PortalOpsInput_save_job_costing": {
        "type": "object",
        "properties": {
          "jobId": {
            "type": "string",
            "description": "Job id from list_jobs / CURRENT JOBS."
          },
          "soldUsd": {
            "type": "number",
            "description": "Customer contract / sold / invoice total in dollars (e.g. 61896). Pass null to clear."
          },
          "totalCostUsd": {
            "type": "number",
            "description": "Optional all-in actual project cost $. When set, GP = sold − this. Pass null to clear."
          },
          "note": {
            "type": "string",
            "description": "Optional short note (source of the figure)."
          }
        },
        "required": [
          "jobId"
        ],
        "title": "save_job_costing"
      },
      "PortalOpsInput_save_numbers_settings": {
        "type": "object",
        "properties": {
          "burdenRatePerHour": {
            "type": "number",
            "description": "Fully-loaded crew cost $/hr."
          },
          "burdenLoadFactor": {
            "type": "number",
            "description": "Load factor used with wage samples (default 1.35)."
          },
          "monthlyOverheadUsd": {
            "type": "number",
            "description": "Fixed monthly overhead $."
          },
          "ownerPayUsd": {
            "type": "number",
            "description": "Owner pay target $/month."
          },
          "targetMarginPct": {
            "type": "number",
            "description": "Target gross margin % 0–100."
          },
          "monthlyProducedTargetUsd": {
            "type": "number",
            "description": "Field `monthlyProducedTargetUsd` — see tool handler and Estimate Builder parity."
          },
          "monthlySoldTargetUsd": {
            "type": "number",
            "description": "Field `monthlySoldTargetUsd` — see tool handler and Estimate Builder parity."
          },
          "monthlyMaterialsBudgetUsd": {
            "type": "number",
            "description": "Field `monthlyMaterialsBudgetUsd` — see tool handler and Estimate Builder parity."
          },
          "monthlyLaborBudgetHours": {
            "type": "number",
            "description": "Field `monthlyLaborBudgetHours` — see tool handler and Estimate Builder parity."
          },
          "budgetAlertPct": {
            "type": "number",
            "description": "Field `budgetAlertPct` — see tool handler and Estimate Builder parity."
          },
          "wageSamples": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string",
                  "description": "Display or job title — for jobs use PO/quote number, not customer name."
                },
                "userId": {
                  "type": "string",
                  "description": "Field `userId` — see tool handler and Estimate Builder parity."
                },
                "hourlyWageUsd": {
                  "type": "number",
                  "description": "Field `hourlyWageUsd` — see tool handler and Estimate Builder parity."
                },
                "annualSalaryUsd": {
                  "type": "number",
                  "description": "Field `annualSalaryUsd` — see tool handler and Estimate Builder parity."
                },
                "weeklyPayUsd": {
                  "type": "number",
                  "description": "Field `weeklyPayUsd` — see tool handler and Estimate Builder parity."
                }
              }
            },
            "description": "Field `wageSamples` — see tool handler and Estimate Builder parity."
          },
          "applySuggestedBurden": {
            "type": "boolean",
            "description": "When wages are set without burden, apply suggested burden. Default true."
          }
        },
        "title": "save_numbers_settings"
      },
      "PortalOpsInput_save_proposal_draft": {
        "type": "object",
        "properties": {
          "estimateId": {
            "type": "string",
            "description": "Draft estimate id from read_proposal / create_estimate."
          },
          "jobId": {
            "type": "string",
            "description": "Job with estimateId — alternative to estimateId."
          },
          "removeAreaIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Remove only these room ids; read_proposal first. Other rooms unchanged."
          },
          "areas": {
            "type": "array",
            "description": "Rooms/surfaces merged by id (substrates with productId, productionRateId, coats). Do not send areas:[] (does not wipe). Each room needs L×W×H or sqft.",
            "items": {
              "type": "object",
              "description": "One room/area on the estimate (hours + products per substrate). Persisted only when length+width+height (or sqft) are present.",
              "properties": {
                "id": {
                  "type": "string",
                  "description": "Room id — merge key when updating an existing draft."
                },
                "name": {
                  "type": "string",
                  "description": "Room label (e.g. Living Room, Exterior south elevation)."
                },
                "length": {
                  "type": "number",
                  "description": "Feet — required with width+height unless sqft provided."
                },
                "width": {
                  "type": "number",
                  "description": "Feet."
                },
                "height": {
                  "type": "number",
                  "description": "Feet (ceiling height)."
                },
                "sqft": {
                  "type": "number",
                  "description": "Optional when L×W×H omitted for flat areas."
                },
                "clientNote": {
                  "type": "string",
                  "description": "Customer-facing scope copy for this room — not a descriptor lineItem. Set \"\" to clear; omit to leave existing text."
                },
                "note": {
                  "type": "string",
                  "description": "Alias of clientNote. Set \"\" to clear; omit to leave existing text."
                },
                "crewNote": {
                  "type": "string",
                  "description": "Internal only — never customer PDF. Set \"\" to clear; omit to leave existing text."
                },
                "isOption": {
                  "type": "boolean",
                  "description": "Optional upgrade room; priced separately when selected."
                },
                "substrates": {
                  "type": "array",
                  "description": "Paint lines in this room — each with productId + productionRateId + coats so hours and material dollars are computed and reach the crew work order. Sending substrates replaces the room's lines (omit to leave unchanged).",
                  "items": {
                    "type": "object",
                    "description": "One paint line inside a room (doors, trim, walls surface, etc.).",
                    "properties": {
                      "id": {
                        "type": "string",
                        "description": "Stable line id within the room (merge key)."
                      },
                      "name": {
                        "type": "string",
                        "description": "Display name (e.g. Walls, Baseboards, Entry door)."
                      },
                      "productId": {
                        "type": "string",
                        "description": "estimateProducts catalog id when set."
                      },
                      "productionRateId": {
                        "type": "string",
                        "description": "productionRates id for labor math."
                      },
                      "coats": {
                        "type": "number",
                        "description": "Coat count for this line."
                      },
                      "qty": {
                        "type": "number",
                        "description": "Item count (doors, units). Same as quantity."
                      },
                      "quantity": {
                        "type": "number",
                        "description": "Alias of qty for doors/trim counts."
                      },
                      "hourOverride": {
                        "type": "number",
                        "description": "Override labor hours for this line (0 allowed)."
                      },
                      "clientNote": {
                        "type": "string",
                        "description": "Customer-visible note for this line only — not crewNote."
                      },
                      "crewNote": {
                        "type": "string",
                        "description": "Internal crew note — never shown on customer proposal."
                      },
                      "isOption": {
                        "type": "boolean",
                        "description": "When true, line is an optional upgrade."
                      }
                    },
                    "additionalProperties": true
                  }
                }
              },
              "additionalProperties": true
            }
          },
          "lineItems": {
            "type": "array",
            "description": "Merge by line id (upsert sent rows; omitted rows kept). replaceLineItems:true for full replace. lineItems:[] is a no-op.",
            "items": {
              "type": "object",
              "description": "Descriptor / option row in the Descriptors section (Estimate Builder shape). See the PRICING RULE in the tool description for when a line carries a flat amount.",
              "properties": {
                "id": {
                  "type": "string",
                  "description": "Line item id."
                },
                "descriptorId": {
                  "type": "string",
                  "description": "Catalog estimateDescriptor id when attached from library."
                },
                "name": {
                  "type": "string",
                  "description": "Customer-visible title."
                },
                "description": {
                  "type": "string",
                  "description": "Customer-visible scope text for this descriptor — do NOT put exclusions here; use exclusions field."
                },
                "amount": {
                  "type": "number",
                  "description": "Flat USD for this line. Saved as sent — the server never rejects a line for its price."
                },
                "hours": {
                  "type": "number",
                  "description": "Labor hours on this line when set."
                },
                "flatAddOn": {
                  "type": "boolean",
                  "description": "true marks a flat-priced add-on line (see the PRICING RULE in the tool description). Stored on the line."
                },
                "isOption": {
                  "type": "boolean",
                  "description": "Optional upgrade descriptor."
                },
                "clientNote": {
                  "type": "string",
                  "description": "Extra customer copy for this descriptor."
                },
                "crewNote": {
                  "type": "string",
                  "description": "Internal crew note."
                }
              },
              "additionalProperties": true
            }
          },
          "removeLineItemIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Remove these lineItems (line id or catalog descriptorId from read_proposal view scope) from a DRAFT estimate; other lines untouched, totals recalculated. Idempotent: ids that match nothing are returned in lineItemIdsNotFound (no error), removed ones in removedLineItemIds. May remove the last line. Sent/accepted estimates return draft-not-editable."
          },
          "patchArea": {
            "description": "Edit ONE existing room by id: { id, ...only the fields to change }. Other rooms and this room's other fields are untouched; substrates listed here patch by substrate id and the room's other substrates stay. A substrate id that is missing or not already on that room returns substrate-not-found (same shape as area-not-found: missingIds, availableIds) and saves nothing — it never appends a surface. Add a new surface through areas. An unknown room id returns area-not-found with availableIds and saves nothing (it never creates a room — use areas for that). Also accepts an array of such objects. Returns patchedAreaIds.",
            "oneOf": [
              {
                "type": "object",
                "description": "One room/area on the estimate (hours + products per substrate). Persisted only when length+width+height (or sqft) are present.",
                "properties": {
                  "id": {
                    "type": "string",
                    "description": "Room id — merge key when updating an existing draft."
                  },
                  "name": {
                    "type": "string",
                    "description": "Room label (e.g. Living Room, Exterior south elevation)."
                  },
                  "length": {
                    "type": "number",
                    "description": "Feet — required with width+height unless sqft provided."
                  },
                  "width": {
                    "type": "number",
                    "description": "Feet."
                  },
                  "height": {
                    "type": "number",
                    "description": "Feet (ceiling height)."
                  },
                  "sqft": {
                    "type": "number",
                    "description": "Optional when L×W×H omitted for flat areas."
                  },
                  "clientNote": {
                    "type": "string",
                    "description": "Customer-facing scope copy for this room — not a descriptor lineItem. Set \"\" to clear; omit to leave existing text."
                  },
                  "note": {
                    "type": "string",
                    "description": "Alias of clientNote. Set \"\" to clear; omit to leave existing text."
                  },
                  "crewNote": {
                    "type": "string",
                    "description": "Internal only — never customer PDF. Set \"\" to clear; omit to leave existing text."
                  },
                  "isOption": {
                    "type": "boolean",
                    "description": "Optional upgrade room; priced separately when selected."
                  },
                  "substrates": {
                    "type": "array",
                    "description": "Paint lines in this room — each with productId + productionRateId + coats so hours and material dollars are computed and reach the crew work order. Sending substrates replaces the room's lines (omit to leave unchanged).",
                    "items": {
                      "type": "object",
                      "description": "One paint line inside a room (doors, trim, walls surface, etc.).",
                      "properties": {
                        "id": {
                          "type": "string",
                          "description": "Stable line id within the room (merge key)."
                        },
                        "name": {
                          "type": "string",
                          "description": "Display name (e.g. Walls, Baseboards, Entry door)."
                        },
                        "productId": {
                          "type": "string",
                          "description": "estimateProducts catalog id when set."
                        },
                        "productionRateId": {
                          "type": "string",
                          "description": "productionRates id for labor math."
                        },
                        "coats": {
                          "type": "number",
                          "description": "Coat count for this line."
                        },
                        "qty": {
                          "type": "number",
                          "description": "Item count (doors, units). Same as quantity."
                        },
                        "quantity": {
                          "type": "number",
                          "description": "Alias of qty for doors/trim counts."
                        },
                        "hourOverride": {
                          "type": "number",
                          "description": "Override labor hours for this line (0 allowed)."
                        },
                        "clientNote": {
                          "type": "string",
                          "description": "Customer-visible note for this line only — not crewNote."
                        },
                        "crewNote": {
                          "type": "string",
                          "description": "Internal crew note — never shown on customer proposal."
                        },
                        "isOption": {
                          "type": "boolean",
                          "description": "When true, line is an optional upgrade."
                        }
                      },
                      "additionalProperties": true
                    }
                  }
                },
                "additionalProperties": true
              },
              {
                "type": "array",
                "items": {
                  "type": "object",
                  "description": "One room/area on the estimate (hours + products per substrate). Persisted only when length+width+height (or sqft) are present.",
                  "properties": {
                    "id": {
                      "type": "string",
                      "description": "Room id — merge key when updating an existing draft."
                    },
                    "name": {
                      "type": "string",
                      "description": "Room label (e.g. Living Room, Exterior south elevation)."
                    },
                    "length": {
                      "type": "number",
                      "description": "Feet — required with width+height unless sqft provided."
                    },
                    "width": {
                      "type": "number",
                      "description": "Feet."
                    },
                    "height": {
                      "type": "number",
                      "description": "Feet (ceiling height)."
                    },
                    "sqft": {
                      "type": "number",
                      "description": "Optional when L×W×H omitted for flat areas."
                    },
                    "clientNote": {
                      "type": "string",
                      "description": "Customer-facing scope copy for this room — not a descriptor lineItem. Set \"\" to clear; omit to leave existing text."
                    },
                    "note": {
                      "type": "string",
                      "description": "Alias of clientNote. Set \"\" to clear; omit to leave existing text."
                    },
                    "crewNote": {
                      "type": "string",
                      "description": "Internal only — never customer PDF. Set \"\" to clear; omit to leave existing text."
                    },
                    "isOption": {
                      "type": "boolean",
                      "description": "Optional upgrade room; priced separately when selected."
                    },
                    "substrates": {
                      "type": "array",
                      "description": "Paint lines in this room — each with productId + productionRateId + coats so hours and material dollars are computed and reach the crew work order. Sending substrates replaces the room's lines (omit to leave unchanged).",
                      "items": {
                        "type": "object",
                        "description": "One paint line inside a room (doors, trim, walls surface, etc.).",
                        "properties": {
                          "id": {
                            "type": "string",
                            "description": "Stable line id within the room (merge key)."
                          },
                          "name": {
                            "type": "string",
                            "description": "Display name (e.g. Walls, Baseboards, Entry door)."
                          },
                          "productId": {
                            "type": "string",
                            "description": "estimateProducts catalog id when set."
                          },
                          "productionRateId": {
                            "type": "string",
                            "description": "productionRates id for labor math."
                          },
                          "coats": {
                            "type": "number",
                            "description": "Coat count for this line."
                          },
                          "qty": {
                            "type": "number",
                            "description": "Item count (doors, units). Same as quantity."
                          },
                          "quantity": {
                            "type": "number",
                            "description": "Alias of qty for doors/trim counts."
                          },
                          "hourOverride": {
                            "type": "number",
                            "description": "Override labor hours for this line (0 allowed)."
                          },
                          "clientNote": {
                            "type": "string",
                            "description": "Customer-visible note for this line only — not crewNote."
                          },
                          "crewNote": {
                            "type": "string",
                            "description": "Internal crew note — never shown on customer proposal."
                          },
                          "isOption": {
                            "type": "boolean",
                            "description": "When true, line is an optional upgrade."
                          }
                        },
                        "additionalProperties": true
                      }
                    }
                  },
                  "additionalProperties": true
                }
              }
            ]
          },
          "patchLineItem": {
            "description": "Edit ONE existing lineItem by id: { id, ...only the fields to change }. Other lines are untouched. An id that is not on the draft returns line-item-not-found with availableIds and saves nothing (it never creates a line — use lineItems for that). Also accepts an array of such objects. Returns patchedLineItemIds.",
            "oneOf": [
              {
                "type": "object",
                "description": "Descriptor / option row in the Descriptors section (Estimate Builder shape). See the PRICING RULE in the tool description for when a line carries a flat amount.",
                "properties": {
                  "id": {
                    "type": "string",
                    "description": "Line item id."
                  },
                  "descriptorId": {
                    "type": "string",
                    "description": "Catalog estimateDescriptor id when attached from library."
                  },
                  "name": {
                    "type": "string",
                    "description": "Customer-visible title."
                  },
                  "description": {
                    "type": "string",
                    "description": "Customer-visible scope text for this descriptor — do NOT put exclusions here; use exclusions field."
                  },
                  "amount": {
                    "type": "number",
                    "description": "Flat USD for this line. Saved as sent — the server never rejects a line for its price."
                  },
                  "hours": {
                    "type": "number",
                    "description": "Labor hours on this line when set."
                  },
                  "flatAddOn": {
                    "type": "boolean",
                    "description": "true marks a flat-priced add-on line (see the PRICING RULE in the tool description). Stored on the line."
                  },
                  "isOption": {
                    "type": "boolean",
                    "description": "Optional upgrade descriptor."
                  },
                  "clientNote": {
                    "type": "string",
                    "description": "Extra customer copy for this descriptor."
                  },
                  "crewNote": {
                    "type": "string",
                    "description": "Internal crew note."
                  }
                },
                "additionalProperties": true
              },
              {
                "type": "array",
                "items": {
                  "type": "object",
                  "description": "Descriptor / option row in the Descriptors section (Estimate Builder shape). See the PRICING RULE in the tool description for when a line carries a flat amount.",
                  "properties": {
                    "id": {
                      "type": "string",
                      "description": "Line item id."
                    },
                    "descriptorId": {
                      "type": "string",
                      "description": "Catalog estimateDescriptor id when attached from library."
                    },
                    "name": {
                      "type": "string",
                      "description": "Customer-visible title."
                    },
                    "description": {
                      "type": "string",
                      "description": "Customer-visible scope text for this descriptor — do NOT put exclusions here; use exclusions field."
                    },
                    "amount": {
                      "type": "number",
                      "description": "Flat USD for this line. Saved as sent — the server never rejects a line for its price."
                    },
                    "hours": {
                      "type": "number",
                      "description": "Labor hours on this line when set."
                    },
                    "flatAddOn": {
                      "type": "boolean",
                      "description": "true marks a flat-priced add-on line (see the PRICING RULE in the tool description). Stored on the line."
                    },
                    "isOption": {
                      "type": "boolean",
                      "description": "Optional upgrade descriptor."
                    },
                    "clientNote": {
                      "type": "string",
                      "description": "Extra customer copy for this descriptor."
                    },
                    "crewNote": {
                      "type": "string",
                      "description": "Internal crew note."
                    }
                  },
                  "additionalProperties": true
                }
              }
            ]
          },
          "replaceLineItems": {
            "type": "boolean",
            "description": "Replace the full lineItems array (keeps explicit replace semantics)."
          },
          "colors": {
            "type": "array",
            "description": "Structured paint colors merged by stable id. replaceColors:true or clearColors:true to replace/clear. colors:[] alone is a no-op.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "description": "Stable row id (backfilled from area+scope+code on read)."
                },
                "areaId": {
                  "type": "string",
                  "description": "Field `areaId` — see tool handler and Estimate Builder parity."
                },
                "areaName": {
                  "type": "string",
                  "description": "Field `areaName` — see tool handler and Estimate Builder parity."
                },
                "scope": {
                  "type": "string",
                  "description": "Field `scope` — see tool handler and Estimate Builder parity."
                },
                "brand": {
                  "type": "string",
                  "description": "Field `brand` — see tool handler and Estimate Builder parity."
                },
                "code": {
                  "type": "string",
                  "description": "Field `code` — see tool handler and Estimate Builder parity."
                },
                "name": {
                  "type": "string",
                  "description": "Display or job title — for jobs use PO/quote number, not customer name."
                },
                "display": {
                  "type": "string",
                  "description": "Field `display` — see tool handler and Estimate Builder parity."
                },
                "notes": {
                  "type": "string",
                  "description": "Field `notes` — see tool handler and Estimate Builder parity."
                },
                "source": {
                  "type": "string",
                  "enum": [
                    "manual",
                    "ai-extract",
                    "ops"
                  ],
                  "description": "Field `source` — see tool handler and Estimate Builder parity."
                },
                "citation": {
                  "type": "string",
                  "description": "Field `citation` — see tool handler and Estimate Builder parity."
                }
              },
              "additionalProperties": true
            }
          },
          "removeColorIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Remove structured color rows by id."
          },
          "replaceColors": {
            "type": "boolean",
            "description": "Replace estimate.colors with the provided array."
          },
          "clearColors": {
            "type": "boolean",
            "description": "Clear all structured colors on the estimate."
          },
          "ifVersion": {
            "type": "integer",
            "description": "Optimistic lock from read_proposal estimate.writeVersion. Mismatch returns 409 version-conflict."
          },
          "dryRun": {
            "type": "boolean",
            "description": "Validate and return changeSummary + totals without writing Firestore."
          },
          "idempotencyKey": {
            "type": "string",
            "description": "Retry key — identical writes within 24h replay the first result. Also via Idempotency-Key header."
          },
          "descriptorIds": {
            "type": "array",
            "description": "Catalog ids from list_estimate_descriptors to merge-add as lineItems.",
            "items": {
              "type": "string"
            }
          },
          "selectedDescriptors": {
            "type": "array",
            "description": "Alias of descriptorIds — string ids or {id,name,description} objects."
          },
          "descriptors": {
            "type": "array",
            "description": "Alias of descriptorIds."
          },
          "addDescriptorIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Field `addDescriptorIds` — see tool handler and Estimate Builder parity."
          },
          "removeDescriptorIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Detach lineItems by catalog descriptorId or line id (draft only). Removed line ids come back in removedDescriptorIds; ids that match nothing in descriptorIdsNotFound (no error). May remove the last line."
          },
          "detachDescriptorIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Alias of removeDescriptorIds."
          },
          "replaceDescriptors": {
            "type": "boolean",
            "description": "Replace base descriptors (keeps isOption rows) before attaching new set."
          },
          "notes": {
            "type": "string",
            "description": "Legacy alias — prefer customerSummary for proposal copy."
          },
          "customerSummary": {
            "type": "string",
            "description": "Customer-facing proposal intro / summary."
          },
          "crewNote": {
            "type": "string",
            "description": "Internal crew-only note — not on customer PDF."
          },
          "exclusions": {
            "description": "Customer-facing exclusions (what is NOT included). One bullet per array entry — never append to descriptor description or notes.",
            "oneOf": [
              {
                "type": "array",
                "items": {
                  "oneOf": [
                    {
                      "type": "string",
                      "description": "Single exclusion bullet text."
                    },
                    {
                      "type": "object",
                      "properties": {
                        "text": {
                          "type": "string"
                        },
                        "id": {
                          "type": "string"
                        }
                      },
                      "additionalProperties": true
                    }
                  ]
                }
              },
              {
                "type": "string",
                "description": "Newline-separated lines; each non-empty line becomes one exclusion bullet."
              }
            ]
          },
          "hourlyRate": {
            "type": "number",
            "description": "Labor rate USD/hr for pricing."
          },
          "taxRate": {
            "type": "number",
            "description": "Tax rate (decimal or percent per org settings)."
          },
          "discount": {
            "description": "Discount object or amount per Estimate Builder."
          },
          "wastePercent": {
            "type": "number",
            "description": "Material waste percent."
          },
          "hoursEstimate": {
            "type": "number",
            "description": "Total estimated hours on the proposal."
          },
          "estimateType": {
            "type": "string",
            "description": "Interior | Exterior | etc."
          },
          "customerName": {
            "type": "string",
            "description": "Contact name — not the job title."
          },
          "customerEmail": {
            "type": "string",
            "description": "Customer email."
          },
          "customerPhone": {
            "type": "string",
            "description": "Customer phone."
          },
          "address": {
            "type": "string",
            "description": "Job site address on the estimate."
          },
          "packages": {
            "type": "array",
            "description": "Optional package definitions when packageMode is true."
          },
          "packageMode": {
            "type": "boolean",
            "description": "Enable good/better/best packages on the proposal."
          },
          "patch": {
            "type": "object",
            "description": "Optional bag of the same draft fields (merged into top level)."
          },
          "showSubstratePriceDescriptors": {
            "type": "boolean",
            "description": "Show per-substrate pricing descriptors on PDF."
          },
          "hideItemized": {
            "type": "boolean",
            "description": "Hide itemized breakdown on customer PDF."
          },
          "clientBuildMode": {
            "type": "boolean",
            "description": "Client-build presentation flag."
          },
          "returnView": {
            "type": "string",
            "enum": [
              "summary",
              "scope",
              "full"
            ],
            "description": "Shape of updatedState on success (default scope)."
          }
        },
        "title": "save_proposal_draft"
      },
      "PortalOpsInput_scan_shifts": {
        "type": "object",
        "properties": {},
        "title": "scan_shifts"
      },
      "PortalOpsInput_score_lead": {
        "type": "object",
        "properties": {
          "jobId": {
            "type": "string",
            "description": "Optional job id to score. Omit to return the top ranked leads."
          },
          "limit": {
            "type": "number",
            "description": "Top leads to return when jobId is omitted. Default 5."
          }
        },
        "title": "score_lead"
      },
      "PortalOpsInput_send_proposal": {
        "type": "object",
        "properties": {
          "estimateId": {
            "type": "string",
            "description": "Estimate document id."
          },
          "jobId": {
            "type": "string",
            "description": "Portal job id."
          },
          "reason": {
            "type": "string",
            "description": "Optional note for audit logs."
          },
          "resolveUnknownNotify": {
            "type": "string",
            "enum": [
              "delivered"
            ],
            "description": "Close an unknown customer-notification claim without emailing or texting. Use a new Idempotency-Key. Omit on a normal send."
          }
        },
        "title": "send_proposal"
      },
      "PortalOpsInput_set_home_file_access": {
        "type": "object",
        "properties": {
          "contactId": {
            "type": "string",
            "description": "Contact to update (single form)."
          },
          "gateCode": {
            "type": "string",
            "description": "Gate or door code. Max 80 characters."
          },
          "lockbox": {
            "type": "string",
            "description": "Lockbox code and where it is. Max 120 characters."
          },
          "accessNotes": {
            "type": "string",
            "description": "How to get in, where to park, alarm, hours. Max 1000 characters."
          },
          "pets": {
            "type": "string",
            "description": "Pets at the home and what the crew should do. Max 500 characters."
          },
          "items": {
            "type": "array",
            "description": "Batch form: one row per contact.",
            "items": {
              "type": "object",
              "properties": {
                "contactId": {
                  "type": "string",
                  "description": "Field `contactId` — see tool handler and Estimate Builder parity."
                },
                "gateCode": {
                  "type": "string",
                  "description": "Field `gateCode` — see tool handler and Estimate Builder parity."
                },
                "lockbox": {
                  "type": "string",
                  "description": "Field `lockbox` — see tool handler and Estimate Builder parity."
                },
                "accessNotes": {
                  "type": "string",
                  "description": "Field `accessNotes` — see tool handler and Estimate Builder parity."
                },
                "pets": {
                  "type": "string",
                  "description": "Field `pets` — see tool handler and Estimate Builder parity."
                }
              },
              "required": [
                "contactId"
              ]
            }
          }
        },
        "required": [],
        "title": "set_home_file_access"
      },
      "PortalOpsInput_set_home_file_formulas": {
        "type": "object",
        "properties": {
          "jobId": {
            "type": "string",
            "description": "Job id or job number (single form)."
          },
          "formulas": {
            "type": "array",
            "description": "Formula rows for this job.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "description": "Existing row id to replace (from read_home_file colors[].formulaId). Omit to add a row."
                },
                "area": {
                  "type": "string",
                  "description": "Area, e.g. Kitchen. Optional."
                },
                "color": {
                  "type": "string",
                  "description": "Color name, e.g. Alabaster."
                },
                "swNumber": {
                  "type": "string",
                  "description": "Sherwin-Williams color number, e.g. SW 7008."
                },
                "sheen": {
                  "type": "string",
                  "description": "Sheen, e.g. Eggshell."
                },
                "productLine": {
                  "type": "string",
                  "description": "Product line, e.g. Duration Home."
                },
                "formula": {
                  "type": "string",
                  "description": "Custom-match formula text (base + colorants). Max 1000 characters."
                }
              }
            }
          },
          "removeIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Row ids to remove."
          },
          "replace": {
            "type": "boolean",
            "description": "true replaces the job's whole list with the rows sent. Default false (add / update by id)."
          },
          "items": {
            "type": "array",
            "description": "Batch form: one row per job (up to 20), each { jobId, formulas, removeIds?, replace? }.",
            "items": {
              "type": "object",
              "properties": {
                "jobId": {
                  "type": "string",
                  "description": "Portal job id."
                },
                "formulas": {
                  "type": "array",
                  "items": {
                    "type": "object"
                  },
                  "description": "Field `formulas` — see tool handler and Estimate Builder parity."
                },
                "removeIds": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  },
                  "description": "Field `removeIds` — see tool handler and Estimate Builder parity."
                },
                "replace": {
                  "type": "boolean",
                  "description": "Field `replace` — see tool handler and Estimate Builder parity."
                }
              },
              "required": [
                "jobId"
              ]
            }
          }
        },
        "required": [],
        "title": "set_home_file_formulas"
      },
      "PortalOpsInput_set_home_file_leftovers": {
        "type": "object",
        "properties": {
          "jobId": {
            "type": "string",
            "description": "Job id or job number (single form)."
          },
          "leftovers": {
            "type": "array",
            "description": "Leftover can rows for this job.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "description": "Existing row id to replace (from read_home_file leftovers[].id). Omit to add a row."
                },
                "color": {
                  "type": "string",
                  "description": "Color on the can."
                },
                "product": {
                  "type": "string",
                  "description": "Product on the can."
                },
                "sheen": {
                  "type": "string",
                  "description": "Field `sheen` — see tool handler and Estimate Builder parity."
                },
                "amount": {
                  "type": "string",
                  "description": "How much is left, e.g. 1 gal about half."
                },
                "location": {
                  "type": "string",
                  "description": "Where it is stored at the house, e.g. garage shelf."
                }
              }
            }
          },
          "removeIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Row ids to remove."
          },
          "replace": {
            "type": "boolean",
            "description": "true replaces the job's whole list with the rows sent. Default false (add / update by id)."
          },
          "items": {
            "type": "array",
            "description": "Batch form: one row per job (up to 20), each { jobId, leftovers, removeIds?, replace? }.",
            "items": {
              "type": "object",
              "properties": {
                "jobId": {
                  "type": "string",
                  "description": "Portal job id."
                },
                "leftovers": {
                  "type": "array",
                  "items": {
                    "type": "object"
                  },
                  "description": "Field `leftovers` — see tool handler and Estimate Builder parity."
                },
                "removeIds": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  },
                  "description": "Field `removeIds` — see tool handler and Estimate Builder parity."
                },
                "replace": {
                  "type": "boolean",
                  "description": "Field `replace` — see tool handler and Estimate Builder parity."
                }
              },
              "required": [
                "jobId"
              ]
            }
          }
        },
        "required": [],
        "title": "set_home_file_leftovers"
      },
      "PortalOpsInput_set_job_colors": {
        "type": "object",
        "properties": {
          "jobId": {
            "type": "string",
            "description": "Portal job id."
          },
          "room": {
            "type": "string",
            "description": "Room name for a single change."
          },
          "color": {
            "type": "string",
            "description": "Paint color for room, e.g. Accessible Beige."
          },
          "sameAsNow": {
            "type": "boolean",
            "description": "True marks room as match existing — no new color."
          },
          "fillAll": {
            "type": "string",
            "description": "Write this color on every room."
          },
          "fillColor": {
            "type": "string",
            "description": "Color for fillRooms."
          },
          "fillRooms": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Room names to fill with fillColor."
          },
          "rooms": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string",
                  "description": "Display or job title — for jobs use PO/quote number, not customer name."
                },
                "color": {
                  "type": "string",
                  "description": "Field `color` — see tool handler and Estimate Builder parity."
                },
                "sameAsNow": {
                  "type": "boolean",
                  "description": "Field `sameAsNow` — see tool handler and Estimate Builder parity."
                }
              }
            },
            "description": "Field `rooms` — see tool handler and Estimate Builder parity."
          },
          "pets": {
            "type": "string",
            "description": "Short pets note."
          },
          "colorConsultRequested": {
            "type": "boolean",
            "description": "Field `colorConsultRequested` — see tool handler and Estimate Builder parity."
          }
        },
        "required": [
          "jobId"
        ],
        "title": "set_job_colors"
      },
      "PortalOpsInput_set_job_name": {
        "type": "object",
        "properties": {
          "jobId": {
            "type": "string",
            "description": "Existing job id (e.g. from list_jobs / create_job)."
          },
          "name": {
            "type": "string",
            "description": "New job title — PO/quote number like '1253'. Never the customer name."
          },
          "jobNumber": {
            "type": "string",
            "description": "Alias for name — PO/quote number."
          },
          "poNumber": {
            "type": "string",
            "description": "Alias for name — PO/quote number."
          },
          "quoteNumber": {
            "type": "string",
            "description": "Alias for name — PO/quote number."
          },
          "customerName": {
            "type": "string",
            "description": "Optional contact field (e.g. 'Kim Brandt'). Does not become the job title."
          },
          "address": {
            "type": "string",
            "description": "Optional job site address update."
          },
          "phone": {
            "type": "string",
            "description": "Optional customer phone."
          },
          "customerPhone": {
            "type": "string",
            "description": "Optional customer phone."
          },
          "email": {
            "type": "string",
            "description": "Optional customer email."
          },
          "customerEmail": {
            "type": "string",
            "description": "Optional customer email."
          }
        },
        "required": [
          "jobId",
          "name"
        ],
        "title": "set_job_name"
      },
      "PortalOpsInput_set_job_status": {
        "type": "object",
        "properties": {
          "jobId": {
            "type": "string",
            "description": "Portal job id."
          },
          "status": {
            "type": "string",
            "enum": [
              "estimate",
              "active",
              "accepted",
              "limbo",
              "completed",
              "archived",
              "complete"
            ],
            "description": "Field `status` — see tool handler and Estimate Builder parity."
          },
          "limboReason": {
            "type": "string",
            "description": "Staff note when parking in limbo (custom text or output from polish_limbo_reason)."
          },
          "generalJob": {
            "type": "boolean",
            "description": "Field `generalJob` — see tool handler and Estimate Builder parity."
          },
          "billingType": {
            "type": "string",
            "enum": [
              "fixed",
              "time_and_materials"
            ],
            "description": "Field `billingType` — see tool handler and Estimate Builder parity."
          }
        },
        "required": [
          "jobId"
        ],
        "title": "set_job_status"
      },
      "PortalOpsInput_suggest_burden_from_wages": {
        "type": "object",
        "properties": {
          "wages": {
            "type": "array",
            "description": "Crew wage samples. Prefer hourlyWageUsd; annualSalaryUsd ÷ 2080 or weeklyPayUsd ÷ 40 also work.",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string",
                  "description": "Crew display name, e.g. ASA or Seth."
                },
                "userId": {
                  "type": "string",
                  "description": "Field `userId` — see tool handler and Estimate Builder parity."
                },
                "hourlyWageUsd": {
                  "type": "number",
                  "description": "Field `hourlyWageUsd` — see tool handler and Estimate Builder parity."
                },
                "annualSalaryUsd": {
                  "type": "number",
                  "description": "Field `annualSalaryUsd` — see tool handler and Estimate Builder parity."
                },
                "weeklyPayUsd": {
                  "type": "number",
                  "description": "Field `weeklyPayUsd` — see tool handler and Estimate Builder parity."
                }
              }
            }
          },
          "loadFactor": {
            "type": "number",
            "description": "Payroll load multiplier. Default 1.35. Range ~1.0–3.0."
          }
        },
        "required": [
          "wages"
        ],
        "title": "suggest_burden_from_wages"
      },
      "PortalOpsInput_toggle_recurring_reminder": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Field `id` — see tool handler and Estimate Builder parity."
          },
          "enabled": {
            "type": "boolean",
            "description": "true = enable / resume, false = pause."
          }
        },
        "required": [
          "id",
          "enabled"
        ],
        "title": "toggle_recurring_reminder"
      },
      "PortalOpsInput_undo_app_import": {
        "type": "object",
        "properties": {
          "importBatchId": {
            "type": "string",
            "description": "importBatchId from commit_app_import or list_app_import_batches."
          }
        },
        "required": [
          "importBatchId"
        ],
        "title": "undo_app_import"
      },
      "PortalOpsInput_unlink_recording": {
        "type": "object",
        "properties": {
          "estimateId": {
            "type": "string",
            "description": "Estimate linked to this job; alternative to jobId/jobRef."
          },
          "jobRef": {
            "type": "string",
            "description": "Field `jobRef` — see tool handler and Estimate Builder parity."
          },
          "jobId": {
            "type": "string",
            "description": "Portal job id."
          },
          "source": {
            "type": "string",
            "enum": [
              "plaud",
              "granola",
              "other"
            ],
            "description": "Field `source` — see tool handler and Estimate Builder parity."
          },
          "recordingId": {
            "type": "string",
            "description": "Field `recordingId` — see tool handler and Estimate Builder parity."
          },
          "dryRun": {
            "type": "boolean",
            "description": "Field `dryRun` — see tool handler and Estimate Builder parity."
          }
        },
        "required": [
          "source",
          "recordingId"
        ],
        "title": "unlink_recording"
      },
      "PortalOpsInput_unsend_proposal_to_draft": {
        "type": "object",
        "properties": {
          "estimateId": {
            "type": "string",
            "description": "Estimate document id."
          },
          "jobId": {
            "type": "string",
            "description": "Portal job id."
          }
        },
        "title": "unsend_proposal_to_draft"
      },
      "PortalOpsInput_update_company_info": {
        "type": "object",
        "properties": {
          "field": {
            "type": "string",
            "enum": [
              "businessName",
              "phone",
              "email",
              "paintStoreName",
              "defaultHourlyRate"
            ],
            "description": "Field `field` — see tool handler and Estimate Builder parity."
          },
          "value": {
            "type": "string",
            "description": "New value as a string (client will coerce type)."
          }
        },
        "required": [
          "field",
          "value"
        ],
        "title": "update_company_info"
      },
      "PortalOpsInput_update_drip_sequence": {
        "type": "object",
        "properties": {
          "sequenceId": {
            "type": "string",
            "description": "Field `sequenceId` — see tool handler and Estimate Builder parity."
          },
          "name": {
            "type": "string",
            "description": "Display or job title — for jobs use PO/quote number, not customer name."
          },
          "enabled": {
            "type": "boolean",
            "description": "Field `enabled` — see tool handler and Estimate Builder parity."
          },
          "trigger": {
            "type": "object",
            "description": "Field `trigger` — see tool handler and Estimate Builder parity."
          },
          "cancelOnEvents": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Field `cancelOnEvents` — see tool handler and Estimate Builder parity."
          },
          "steps": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Field `steps` — see tool handler and Estimate Builder parity."
          }
        },
        "required": [
          "sequenceId"
        ],
        "title": "update_drip_sequence"
      },
      "PortalOpsInput_update_email_template": {
        "type": "object",
        "properties": {
          "kind": {
            "type": "string",
            "description": "Field `kind` — see tool handler and Estimate Builder parity."
          },
          "subject": {
            "type": "string",
            "description": "Field `subject` — see tool handler and Estimate Builder parity."
          },
          "heading": {
            "type": "string",
            "description": "Field `heading` — see tool handler and Estimate Builder parity."
          },
          "body": {
            "type": "string",
            "description": "Field `body` — see tool handler and Estimate Builder parity."
          },
          "ctaLabel": {
            "type": "string",
            "description": "Field `ctaLabel` — see tool handler and Estimate Builder parity."
          },
          "sms": {
            "type": "string",
            "description": "Field `sms` — see tool handler and Estimate Builder parity."
          }
        },
        "required": [
          "kind",
          "subject",
          "body"
        ],
        "title": "update_email_template"
      },
      "PortalOpsInput_update_estimate": {
        "type": "object",
        "properties": {
          "jobRef": {
            "type": "string",
            "description": "PO/quote number, customer name, address fragment, or jobId."
          },
          "jobId": {
            "type": "string",
            "description": "Optional explicit jobId instead of jobRef."
          },
          "estimateId": {
            "type": "string",
            "description": "Estimate document id."
          },
          "removeAreaIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Field `removeAreaIds` — see tool handler and Estimate Builder parity."
          },
          "removeColorIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Field `removeColorIds` — see tool handler and Estimate Builder parity."
          },
          "removeDescriptorIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Detach lineItems by catalog descriptorId or line id (draft only). Removed line ids come back in removedDescriptorIds; ids that match nothing in descriptorIdsNotFound (no error). May remove the last line."
          },
          "removeLineItemIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Remove these lineItems (line id or catalog descriptorId from read_proposal view scope) from a DRAFT estimate; other lines untouched, totals recalculated. Idempotent: ids that match nothing are returned in lineItemIdsNotFound (no error), removed ones in removedLineItemIds. May remove the last line. Sent/accepted estimates return draft-not-editable."
          },
          "patchArea": {
            "description": "Edit ONE existing room by id: { id, ...only the fields to change }. Other rooms and this room's other fields are untouched; substrates listed here patch by substrate id and the room's other substrates stay. A substrate id that is missing or not already on that room returns substrate-not-found (same shape as area-not-found: missingIds, availableIds) and saves nothing — it never appends a surface. Add a new surface through areas. An unknown room id returns area-not-found with availableIds and saves nothing (it never creates a room — use areas for that). Also accepts an array of such objects. Returns patchedAreaIds.",
            "oneOf": [
              {
                "type": "object",
                "description": "One room/area on the estimate (hours + products per substrate). Persisted only when length+width+height (or sqft) are present.",
                "properties": {
                  "id": {
                    "type": "string",
                    "description": "Room id — merge key when updating an existing draft."
                  },
                  "name": {
                    "type": "string",
                    "description": "Room label (e.g. Living Room, Exterior south elevation)."
                  },
                  "length": {
                    "type": "number",
                    "description": "Feet — required with width+height unless sqft provided."
                  },
                  "width": {
                    "type": "number",
                    "description": "Feet."
                  },
                  "height": {
                    "type": "number",
                    "description": "Feet (ceiling height)."
                  },
                  "sqft": {
                    "type": "number",
                    "description": "Optional when L×W×H omitted for flat areas."
                  },
                  "clientNote": {
                    "type": "string",
                    "description": "Customer-facing scope copy for this room — not a descriptor lineItem. Set \"\" to clear; omit to leave existing text."
                  },
                  "note": {
                    "type": "string",
                    "description": "Alias of clientNote. Set \"\" to clear; omit to leave existing text."
                  },
                  "crewNote": {
                    "type": "string",
                    "description": "Internal only — never customer PDF. Set \"\" to clear; omit to leave existing text."
                  },
                  "isOption": {
                    "type": "boolean",
                    "description": "Optional upgrade room; priced separately when selected."
                  },
                  "substrates": {
                    "type": "array",
                    "description": "Paint lines in this room — each with productId + productionRateId + coats so hours and material dollars are computed and reach the crew work order. Sending substrates replaces the room's lines (omit to leave unchanged).",
                    "items": {
                      "type": "object",
                      "description": "One paint line inside a room (doors, trim, walls surface, etc.).",
                      "properties": {
                        "id": {
                          "type": "string",
                          "description": "Stable line id within the room (merge key)."
                        },
                        "name": {
                          "type": "string",
                          "description": "Display name (e.g. Walls, Baseboards, Entry door)."
                        },
                        "productId": {
                          "type": "string",
                          "description": "estimateProducts catalog id when set."
                        },
                        "productionRateId": {
                          "type": "string",
                          "description": "productionRates id for labor math."
                        },
                        "coats": {
                          "type": "number",
                          "description": "Coat count for this line."
                        },
                        "qty": {
                          "type": "number",
                          "description": "Item count (doors, units). Same as quantity."
                        },
                        "quantity": {
                          "type": "number",
                          "description": "Alias of qty for doors/trim counts."
                        },
                        "hourOverride": {
                          "type": "number",
                          "description": "Override labor hours for this line (0 allowed)."
                        },
                        "clientNote": {
                          "type": "string",
                          "description": "Customer-visible note for this line only — not crewNote."
                        },
                        "crewNote": {
                          "type": "string",
                          "description": "Internal crew note — never shown on customer proposal."
                        },
                        "isOption": {
                          "type": "boolean",
                          "description": "When true, line is an optional upgrade."
                        }
                      },
                      "additionalProperties": true
                    }
                  }
                },
                "additionalProperties": true
              },
              {
                "type": "array",
                "items": {
                  "type": "object",
                  "description": "One room/area on the estimate (hours + products per substrate). Persisted only when length+width+height (or sqft) are present.",
                  "properties": {
                    "id": {
                      "type": "string",
                      "description": "Room id — merge key when updating an existing draft."
                    },
                    "name": {
                      "type": "string",
                      "description": "Room label (e.g. Living Room, Exterior south elevation)."
                    },
                    "length": {
                      "type": "number",
                      "description": "Feet — required with width+height unless sqft provided."
                    },
                    "width": {
                      "type": "number",
                      "description": "Feet."
                    },
                    "height": {
                      "type": "number",
                      "description": "Feet (ceiling height)."
                    },
                    "sqft": {
                      "type": "number",
                      "description": "Optional when L×W×H omitted for flat areas."
                    },
                    "clientNote": {
                      "type": "string",
                      "description": "Customer-facing scope copy for this room — not a descriptor lineItem. Set \"\" to clear; omit to leave existing text."
                    },
                    "note": {
                      "type": "string",
                      "description": "Alias of clientNote. Set \"\" to clear; omit to leave existing text."
                    },
                    "crewNote": {
                      "type": "string",
                      "description": "Internal only — never customer PDF. Set \"\" to clear; omit to leave existing text."
                    },
                    "isOption": {
                      "type": "boolean",
                      "description": "Optional upgrade room; priced separately when selected."
                    },
                    "substrates": {
                      "type": "array",
                      "description": "Paint lines in this room — each with productId + productionRateId + coats so hours and material dollars are computed and reach the crew work order. Sending substrates replaces the room's lines (omit to leave unchanged).",
                      "items": {
                        "type": "object",
                        "description": "One paint line inside a room (doors, trim, walls surface, etc.).",
                        "properties": {
                          "id": {
                            "type": "string",
                            "description": "Stable line id within the room (merge key)."
                          },
                          "name": {
                            "type": "string",
                            "description": "Display name (e.g. Walls, Baseboards, Entry door)."
                          },
                          "productId": {
                            "type": "string",
                            "description": "estimateProducts catalog id when set."
                          },
                          "productionRateId": {
                            "type": "string",
                            "description": "productionRates id for labor math."
                          },
                          "coats": {
                            "type": "number",
                            "description": "Coat count for this line."
                          },
                          "qty": {
                            "type": "number",
                            "description": "Item count (doors, units). Same as quantity."
                          },
                          "quantity": {
                            "type": "number",
                            "description": "Alias of qty for doors/trim counts."
                          },
                          "hourOverride": {
                            "type": "number",
                            "description": "Override labor hours for this line (0 allowed)."
                          },
                          "clientNote": {
                            "type": "string",
                            "description": "Customer-visible note for this line only — not crewNote."
                          },
                          "crewNote": {
                            "type": "string",
                            "description": "Internal crew note — never shown on customer proposal."
                          },
                          "isOption": {
                            "type": "boolean",
                            "description": "When true, line is an optional upgrade."
                          }
                        },
                        "additionalProperties": true
                      }
                    }
                  },
                  "additionalProperties": true
                }
              }
            ]
          },
          "patchLineItem": {
            "description": "Edit ONE existing lineItem by id: { id, ...only the fields to change }. Other lines are untouched. An id that is not on the draft returns line-item-not-found with availableIds and saves nothing (it never creates a line — use lineItems for that). Also accepts an array of such objects. Returns patchedLineItemIds.",
            "oneOf": [
              {
                "type": "object",
                "description": "Descriptor / option row in the Descriptors section (Estimate Builder shape). See the PRICING RULE in the tool description for when a line carries a flat amount.",
                "properties": {
                  "id": {
                    "type": "string",
                    "description": "Line item id."
                  },
                  "descriptorId": {
                    "type": "string",
                    "description": "Catalog estimateDescriptor id when attached from library."
                  },
                  "name": {
                    "type": "string",
                    "description": "Customer-visible title."
                  },
                  "description": {
                    "type": "string",
                    "description": "Customer-visible scope text for this descriptor — do NOT put exclusions here; use exclusions field."
                  },
                  "amount": {
                    "type": "number",
                    "description": "Flat USD for this line. Saved as sent — the server never rejects a line for its price."
                  },
                  "hours": {
                    "type": "number",
                    "description": "Labor hours on this line when set."
                  },
                  "flatAddOn": {
                    "type": "boolean",
                    "description": "true marks a flat-priced add-on line (see the PRICING RULE in the tool description). Stored on the line."
                  },
                  "isOption": {
                    "type": "boolean",
                    "description": "Optional upgrade descriptor."
                  },
                  "clientNote": {
                    "type": "string",
                    "description": "Extra customer copy for this descriptor."
                  },
                  "crewNote": {
                    "type": "string",
                    "description": "Internal crew note."
                  }
                },
                "additionalProperties": true
              },
              {
                "type": "array",
                "items": {
                  "type": "object",
                  "description": "Descriptor / option row in the Descriptors section (Estimate Builder shape). See the PRICING RULE in the tool description for when a line carries a flat amount.",
                  "properties": {
                    "id": {
                      "type": "string",
                      "description": "Line item id."
                    },
                    "descriptorId": {
                      "type": "string",
                      "description": "Catalog estimateDescriptor id when attached from library."
                    },
                    "name": {
                      "type": "string",
                      "description": "Customer-visible title."
                    },
                    "description": {
                      "type": "string",
                      "description": "Customer-visible scope text for this descriptor — do NOT put exclusions here; use exclusions field."
                    },
                    "amount": {
                      "type": "number",
                      "description": "Flat USD for this line. Saved as sent — the server never rejects a line for its price."
                    },
                    "hours": {
                      "type": "number",
                      "description": "Labor hours on this line when set."
                    },
                    "flatAddOn": {
                      "type": "boolean",
                      "description": "true marks a flat-priced add-on line (see the PRICING RULE in the tool description). Stored on the line."
                    },
                    "isOption": {
                      "type": "boolean",
                      "description": "Optional upgrade descriptor."
                    },
                    "clientNote": {
                      "type": "string",
                      "description": "Extra customer copy for this descriptor."
                    },
                    "crewNote": {
                      "type": "string",
                      "description": "Internal crew note."
                    }
                  },
                  "additionalProperties": true
                }
              }
            ]
          },
          "areas": {
            "type": "array",
            "description": "Rooms/surfaces merged by id (substrates with productId, productionRateId, coats). Do not send areas:[] (does not wipe). Each room needs L×W×H or sqft.",
            "items": {
              "type": "object",
              "description": "One room/area on the estimate (hours + products per substrate). Persisted only when length+width+height (or sqft) are present.",
              "properties": {
                "id": {
                  "type": "string",
                  "description": "Room id — merge key when updating an existing draft."
                },
                "name": {
                  "type": "string",
                  "description": "Room label (e.g. Living Room, Exterior south elevation)."
                },
                "length": {
                  "type": "number",
                  "description": "Feet — required with width+height unless sqft provided."
                },
                "width": {
                  "type": "number",
                  "description": "Feet."
                },
                "height": {
                  "type": "number",
                  "description": "Feet (ceiling height)."
                },
                "sqft": {
                  "type": "number",
                  "description": "Optional when L×W×H omitted for flat areas."
                },
                "clientNote": {
                  "type": "string",
                  "description": "Customer-facing scope copy for this room — not a descriptor lineItem. Set \"\" to clear; omit to leave existing text."
                },
                "note": {
                  "type": "string",
                  "description": "Alias of clientNote. Set \"\" to clear; omit to leave existing text."
                },
                "crewNote": {
                  "type": "string",
                  "description": "Internal only — never customer PDF. Set \"\" to clear; omit to leave existing text."
                },
                "isOption": {
                  "type": "boolean",
                  "description": "Optional upgrade room; priced separately when selected."
                },
                "substrates": {
                  "type": "array",
                  "description": "Paint lines in this room — each with productId + productionRateId + coats so hours and material dollars are computed and reach the crew work order. Sending substrates replaces the room's lines (omit to leave unchanged).",
                  "items": {
                    "type": "object",
                    "description": "One paint line inside a room (doors, trim, walls surface, etc.).",
                    "properties": {
                      "id": {
                        "type": "string",
                        "description": "Stable line id within the room (merge key)."
                      },
                      "name": {
                        "type": "string",
                        "description": "Display name (e.g. Walls, Baseboards, Entry door)."
                      },
                      "productId": {
                        "type": "string",
                        "description": "estimateProducts catalog id when set."
                      },
                      "productionRateId": {
                        "type": "string",
                        "description": "productionRates id for labor math."
                      },
                      "coats": {
                        "type": "number",
                        "description": "Coat count for this line."
                      },
                      "qty": {
                        "type": "number",
                        "description": "Item count (doors, units). Same as quantity."
                      },
                      "quantity": {
                        "type": "number",
                        "description": "Alias of qty for doors/trim counts."
                      },
                      "hourOverride": {
                        "type": "number",
                        "description": "Override labor hours for this line (0 allowed)."
                      },
                      "clientNote": {
                        "type": "string",
                        "description": "Customer-visible note for this line only — not crewNote."
                      },
                      "crewNote": {
                        "type": "string",
                        "description": "Internal crew note — never shown on customer proposal."
                      },
                      "isOption": {
                        "type": "boolean",
                        "description": "When true, line is an optional upgrade."
                      }
                    },
                    "additionalProperties": true
                  }
                }
              },
              "additionalProperties": true
            }
          },
          "lineItems": {
            "type": "array",
            "description": "Merge by line id (upsert sent rows; omitted rows kept). replaceLineItems:true for full replace. lineItems:[] is a no-op.",
            "items": {
              "type": "object",
              "description": "Descriptor / option row in the Descriptors section (Estimate Builder shape). See the PRICING RULE in the tool description for when a line carries a flat amount.",
              "properties": {
                "id": {
                  "type": "string",
                  "description": "Line item id."
                },
                "descriptorId": {
                  "type": "string",
                  "description": "Catalog estimateDescriptor id when attached from library."
                },
                "name": {
                  "type": "string",
                  "description": "Customer-visible title."
                },
                "description": {
                  "type": "string",
                  "description": "Customer-visible scope text for this descriptor — do NOT put exclusions here; use exclusions field."
                },
                "amount": {
                  "type": "number",
                  "description": "Flat USD for this line. Saved as sent — the server never rejects a line for its price."
                },
                "hours": {
                  "type": "number",
                  "description": "Labor hours on this line when set."
                },
                "flatAddOn": {
                  "type": "boolean",
                  "description": "true marks a flat-priced add-on line (see the PRICING RULE in the tool description). Stored on the line."
                },
                "isOption": {
                  "type": "boolean",
                  "description": "Optional upgrade descriptor."
                },
                "clientNote": {
                  "type": "string",
                  "description": "Extra customer copy for this descriptor."
                },
                "crewNote": {
                  "type": "string",
                  "description": "Internal crew note."
                }
              },
              "additionalProperties": true
            }
          },
          "descriptorIds": {
            "type": "array",
            "description": "Field `descriptorIds` — see tool handler and Estimate Builder parity."
          },
          "selectedDescriptors": {
            "type": "array",
            "description": "Field `selectedDescriptors` — see tool handler and Estimate Builder parity."
          },
          "descriptors": {
            "type": "array",
            "description": "Field `descriptors` — see tool handler and Estimate Builder parity."
          },
          "addDescriptorIds": {
            "type": "array",
            "description": "Field `addDescriptorIds` — see tool handler and Estimate Builder parity."
          },
          "colors": {
            "type": "array",
            "description": "Field `colors` — see tool handler and Estimate Builder parity."
          },
          "exclusions": {
            "type": "string",
            "description": "Field `exclusions` — see tool handler and Estimate Builder parity."
          },
          "notes": {
            "type": "string",
            "description": "Field `notes` — see tool handler and Estimate Builder parity."
          },
          "customerSummary": {
            "type": "string",
            "description": "Field `customerSummary` — see tool handler and Estimate Builder parity."
          },
          "crewNote": {
            "type": "string",
            "description": "Field `crewNote` — see tool handler and Estimate Builder parity."
          },
          "hourlyRate": {
            "type": "number",
            "description": "Field `hourlyRate` — see tool handler and Estimate Builder parity."
          },
          "taxRate": {
            "type": "number",
            "description": "Field `taxRate` — see tool handler and Estimate Builder parity."
          },
          "discount": {
            "description": "Field `discount` — see tool handler and Estimate Builder parity."
          },
          "wastePercent": {
            "type": "number",
            "description": "Field `wastePercent` — see tool handler and Estimate Builder parity."
          },
          "hoursEstimate": {
            "type": "number",
            "description": "Field `hoursEstimate` — see tool handler and Estimate Builder parity."
          },
          "patch": {
            "type": "object",
            "description": "Field `patch` — see tool handler and Estimate Builder parity."
          }
        },
        "title": "update_estimate"
      },
      "PortalOpsInput_upload_job_file": {
        "type": "object",
        "properties": {
          "jobId": {
            "type": "string",
            "description": "Existing job id from add_job / list_jobs."
          },
          "pdfBase64": {
            "type": "string",
            "description": "Base64 PDF bytes (or data:application/pdf;base64,…)."
          },
          "fileBase64": {
            "type": "string",
            "description": "Alias for pdfBase64."
          },
          "fileName": {
            "type": "string",
            "description": "Filename, e.g. work-order-1310-Kim.pdf."
          },
          "kind": {
            "type": "string",
            "description": "Optional; default work-order."
          }
        },
        "required": [
          "jobId",
          "pdfBase64"
        ],
        "title": "upload_job_file"
      },
      "PortalOpsInput_upload_photo": {
        "type": "object",
        "properties": {
          "scope": {
            "type": "string",
            "enum": [
              "job",
              "estimate"
            ],
            "description": "Compatibility target selector; jobId/estimateId identify the target."
          },
          "jobId": {
            "type": "string",
            "description": "Job to attach to (or pass estimateId)."
          },
          "estimateId": {
            "type": "string",
            "description": "Estimate to attach to; its job is used when set."
          },
          "areaId": {
            "type": "string",
            "description": "Estimate area id (needed to place it on the proposal once approved)."
          },
          "substrateId": {
            "type": "string",
            "description": "Estimate substrate id inside areaId (optional)."
          },
          "imageBase64": {
            "type": "string",
            "description": "Image bytes, base64 (data: prefix allowed)."
          },
          "contentType": {
            "type": "string",
            "description": "Informational; the real type is detected from the bytes."
          },
          "fileName": {
            "type": "string",
            "description": "Field `fileName` — see tool handler and Estimate Builder parity."
          },
          "room": {
            "type": "string",
            "description": "Room / area label."
          },
          "caption": {
            "type": "string",
            "description": "Field `caption` — see tool handler and Estimate Builder parity."
          },
          "takenAt": {
            "type": "number",
            "description": "Epoch ms the photo was taken."
          }
        },
        "required": [
          "imageBase64"
        ],
        "title": "upload_photo"
      },
      "PortalOpsInput_upsert_estimate_descriptor": {
        "type": "object",
        "properties": {
          "scope": {
            "type": "string",
            "enum": [
              "library"
            ],
            "description": "Optional explicit confirmation that this edits the shared descriptor library. It does not allow a call that also sends an estimate/proposal/job/customer key — that is always rejected. price and flatAddOn may be stored for an add-on such as a lift rental; area pricing goes through hours and materials."
          },
          "id": {
            "type": "string",
            "description": "Existing descriptor id when editing."
          },
          "name": {
            "type": "string",
            "description": "Title / Label (required on create)."
          },
          "description": {
            "type": "string",
            "description": "Full body text shown on proposals (required on create)."
          },
          "crewNote": {
            "type": "string",
            "description": "Field `crewNote` — see tool handler and Estimate Builder parity."
          },
          "clientNote": {
            "type": "string",
            "description": "Field `clientNote` — see tool handler and Estimate Builder parity."
          },
          "materials": {
            "type": "string",
            "description": "Field `materials` — see tool handler and Estimate Builder parity."
          },
          "estimateTypes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Field `estimateTypes` — see tool handler and Estimate Builder parity."
          },
          "sort": {
            "type": "number",
            "description": "Field `sort` — see tool handler and Estimate Builder parity."
          },
          "price": {
            "type": "number",
            "description": "Flat add-on price only (lift or equipment rental). Area pricing goes through hours and materials on the estimate, not this field."
          },
          "flatAddOn": {
            "type": "boolean",
            "description": "Set true with price for an add-on such as a lift rental. Painting and staining are priced through areas."
          }
        },
        "title": "upsert_estimate_descriptor"
      },
      "PortalOpsInput_upsert_estimate_product": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Existing product id when editing."
          },
          "name": {
            "type": "string",
            "description": "Catalog name (required on create)."
          },
          "group": {
            "type": "string",
            "description": "Field `group` — see tool handler and Estimate Builder parity."
          },
          "finish": {
            "type": "string",
            "description": "Field `finish` — see tool handler and Estimate Builder parity."
          },
          "coveragePerUnit": {
            "type": "number",
            "description": "sqft per gallon (required on create)."
          },
          "coverageLnft": {
            "type": "number",
            "description": "Field `coverageLnft` — see tool handler and Estimate Builder parity."
          },
          "galPerItem": {
            "type": "number",
            "description": "Gallons per item (doors/windows)."
          },
          "pricePerUnit": {
            "type": "number",
            "description": "$ per gallon (required on create)."
          },
          "unit": {
            "type": "string",
            "description": "Usually gal."
          },
          "swatch": {
            "type": "string",
            "description": "Field `swatch` — see tool handler and Estimate Builder parity."
          },
          "productKey": {
            "type": "string",
            "description": "Field `productKey` — see tool handler and Estimate Builder parity."
          },
          "pds": {
            "type": "object",
            "description": "Optional PDS thresholds object from research_estimate_product."
          },
          "estimateTypes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Field `estimateTypes` — see tool handler and Estimate Builder parity."
          },
          "sort": {
            "type": "number",
            "description": "Field `sort` — see tool handler and Estimate Builder parity."
          }
        },
        "title": "upsert_estimate_product"
      },
      "PortalOpsInput_upsert_production_rate": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Existing rate id when editing."
          },
          "name": {
            "type": "string",
            "description": "Internal label (required on create)."
          },
          "group": {
            "type": "string",
            "description": "e.g. Interior, Ceilings, Exterior."
          },
          "type": {
            "type": "string",
            "enum": [
              "sqft_hr",
              "lnft_hr",
              "hrs_item",
              "dollar_sqft",
              "dollar_item"
            ],
            "description": "Field `type` — see tool handler and Estimate Builder parity."
          },
          "measureMode": {
            "type": "string",
            "enum": [
              "walls",
              "ceiling",
              "floor",
              "base",
              "item"
            ],
            "description": "Field `measureMode` — see tool handler and Estimate Builder parity."
          },
          "unitLabel": {
            "type": "string",
            "description": "e.g. sqft/hr, lnft/hr, hr/item, $/sqft, $/item."
          },
          "ratesByCoats": {
            "type": "object",
            "description": "Coat count → rate number. Keys are coat counts as strings.",
            "additionalProperties": {
              "type": "number"
            }
          },
          "clientLabel": {
            "type": "string",
            "description": "Customer-facing name on proposals."
          },
          "defaultCoats": {
            "type": "number",
            "description": "Field `defaultCoats` — see tool handler and Estimate Builder parity."
          },
          "defaultProductId": {
            "type": "string",
            "description": "Field `defaultProductId` — see tool handler and Estimate Builder parity."
          },
          "prepHours": {
            "type": "number",
            "description": "Field `prepHours` — see tool handler and Estimate Builder parity."
          },
          "defaultQty": {
            "type": "number",
            "description": "When measureMode is item."
          },
          "crewNote": {
            "type": "string",
            "description": "Field `crewNote` — see tool handler and Estimate Builder parity."
          },
          "areaDescription": {
            "type": "string",
            "description": "Field `areaDescription` — see tool handler and Estimate Builder parity."
          },
          "substrateDescription": {
            "type": "string",
            "description": "Field `substrateDescription` — see tool handler and Estimate Builder parity."
          },
          "estimateTypes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Field `estimateTypes` — see tool handler and Estimate Builder parity."
          },
          "sort": {
            "type": "number",
            "description": "Field `sort` — see tool handler and Estimate Builder parity."
          },
          "standard": {
            "type": "boolean",
            "description": "When true, Estimate AI prefers this rate the same way it prefers names ending with Prep and Paint."
          }
        },
        "title": "upsert_production_rate"
      },
      "PortalOpsInput_void_receipt": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "receiptId of a confirmed receipt."
          },
          "reason": {
            "type": "string",
            "description": "Why it is being voided. Written on the reversal entry."
          }
        },
        "required": [
          "id",
          "reason"
        ],
        "title": "void_receipt"
      }
    }
  }
}
