{"openapi":"3.0.0","info":{"title":"Purple Suite API","version":"1.0.0","description":"Every PurpleSuite source-system API in one document — 23 apps, merged from the specs each app serves at `/api/purple-suite/{app}/openapi.json`.\n\n**Paths are absolute from the `/api` root.** Each app's operations are prefixed with `/purple-suite/{app}`, so ITSM's `/incidents` appears here as `/purple-suite/itsm/incidents`. Prefixing is what lets apps that share a relative path (`/users` in chat, identity and pm) coexist in one document.\n\n**Component keys are namespaced `{app}.{Name}`** (`crm.Account`, `retail-banking.Account`) because same-named schemas across apps are not the same shape. Every operation is tagged with its app id, and `x-purple-suite-apps` maps each app to its title, path prefix and single-app spec URL.\n\n**Auth.** Send the instance's Personal Access Token as `Authorization: Bearer <PAT>` plus the `X-Instance-ID` header. Create an instance (and get a PAT) with the Instance Management API at `/api/purple-suite/instances/openapi.json`.\n\n**Subsetting.** `?apps=itsm,expense` returns the same document narrowed to those apps — useful for clients with a document-size limit.\n\nThis document is generated at request time from the live app specs; it is always consistent with the per-app documents.\n\n---\n\n**Acting as a user (`X-PS-Impersonate-User`).** Every endpoint accepts an optional `X-PS-Impersonate-User` request header whose value is the email of the instance user to act as. The effective user resolves in this order: the header if present, else the instance's configured default MCP user, else none. User-scoped paths additionally accept the literal `me` in place of an email, and Graph-style apps also serve `/me/…` twins of their `/users/{email}/…` paths that resolve the same way. This header is honored by all operations but is intentionally omitted from each operation's parameter list so it is not surfaced as a per-tool argument by MCP clients; see the `ImpersonateUser` entry under `components.parameters`."},"servers":[{"url":"https://marketplace.moveworks.com/api","description":"PurpleSuite API root"}],"security":[{"patAuth":[]}],"tags":[{"name":"ats","description":"Purple Suite ATS API"},{"name":"bank-ops","description":"Purple Bank Ops API"},{"name":"calendar","description":"Purple Suite Calendar API"},{"name":"chat","description":"Purple Suite Chat API"},{"name":"community","description":"Purple Suite Community API"},{"name":"crm","description":"Purple Suite CRM API"},{"name":"dex","description":"Purple Suite DEX API"},{"name":"erp","description":"Purple Suite ERP API"},{"name":"expense","description":"Purple Expense API"},{"name":"grid","description":"Purple Suite Grid API"},{"name":"hrbp","description":"Purple Suite HRBP API"},{"name":"hris","description":"Purple Suite HRIS API"},{"name":"identity","description":"Purple Identity API"},{"name":"itsm","description":"Purple Suite ITSM API"},{"name":"lms","description":"Purple Suite LMS API"},{"name":"manager-experience","description":"Purple Suite Manager Experience API"},{"name":"pages","description":"Purple Suite Pages API"},{"name":"pm","description":"Purple Suite PM API"},{"name":"purple-talent","description":"Purple Suite PurpleTalent API"},{"name":"retail-banking","description":"Purple Retail Banking API"},{"name":"risk-compliance","description":"Purple Risk & Compliance API"},{"name":"scm","description":"Purple Suite SCM API"},{"name":"tasks-approvals","description":"Purple Suite Tasks & Approvals API"}],"components":{"securitySchemes":{"patAuth":{"type":"http","scheme":"bearer"}},"parameters":{"ats.InstanceId":{"in":"header","name":"X-Instance-ID","required":true,"schema":{"type":"string"}},"bank-ops.InstanceId":{"in":"header","name":"X-Instance-ID","required":true,"schema":{"type":"string"}},"calendar.InstanceId":{"in":"header","name":"X-Instance-ID","required":true,"schema":{"type":"string"}},"calendar.Top":{"in":"query","name":"$top","schema":{"type":"integer","default":50},"description":"Number of items to return"},"calendar.Skip":{"in":"query","name":"$skip","schema":{"type":"integer","default":0},"description":"Number of items to skip"},"chat.InstanceId":{"in":"header","name":"X-Instance-ID","required":true,"schema":{"type":"string","example":"inst_abc123"},"description":"The Purple Suite instance ID that scopes all subcollections"},"community.InstanceId":{"in":"header","name":"X-Instance-ID","required":true,"schema":{"type":"string"}},"crm.InstanceId":{"in":"header","name":"X-Instance-ID","required":true,"schema":{"type":"string"}},"dex.InstanceId":{"in":"header","name":"X-Instance-ID","required":true,"schema":{"type":"string"}},"erp.InstanceId":{"in":"header","name":"X-Instance-ID","required":true,"schema":{"type":"string"}},"expense.InstanceId":{"in":"header","name":"X-Instance-ID","required":true,"schema":{"type":"string"}},"grid.InstanceId":{"in":"header","name":"X-Instance-ID","required":true,"schema":{"type":"string"}},"hrbp.InstanceId":{"in":"header","name":"X-Instance-ID","required":true,"schema":{"type":"string"}},"hris.InstanceId":{"in":"header","name":"X-Instance-ID","required":true,"schema":{"type":"string"}},"identity.InstanceId":{"in":"header","name":"X-Instance-ID","required":true,"schema":{"type":"string"}},"itsm.InstanceId":{"in":"header","name":"X-Instance-ID","required":true,"schema":{"type":"string"}},"lms.InstanceId":{"in":"header","name":"X-Instance-ID","required":true,"schema":{"type":"string"}},"manager-experience.InstanceId":{"in":"header","name":"X-Instance-ID","required":true,"schema":{"type":"string"}},"pages.InstanceId":{"in":"header","name":"X-Instance-ID","required":true,"schema":{"type":"string"},"example":"pages-instance-abc123"},"pm.InstanceId":{"in":"header","name":"X-Instance-ID","required":true,"schema":{"type":"string"}},"purple-talent.InstanceId":{"in":"header","name":"X-Instance-ID","required":true,"schema":{"type":"string"}},"retail-banking.InstanceId":{"in":"header","name":"X-Instance-ID","required":true,"schema":{"type":"string"}},"risk-compliance.InstanceId":{"in":"header","name":"X-Instance-ID","required":true,"schema":{"type":"string"}},"scm.InstanceId":{"in":"header","name":"X-Instance-ID","required":true,"schema":{"type":"string"}},"tasks-approvals.InstanceId":{"in":"header","name":"X-Instance-ID","required":true,"schema":{"type":"string"}},"ImpersonateUser":{"in":"header","name":"X-PS-Impersonate-User","required":false,"schema":{"type":"string","format":"email"},"description":"Optional. Act as a specific user in the target instance — the value is that user's email (must exist in the instance). Resolution order for the effective user: (1) this header, if set; (2) the instance's configured default MCP user; (3) otherwise no user is bound. User-scoped paths (e.g. /users/{email}/…) also accept the literal \"me\", and Graph-style apps serve /me/… twins of those paths (Graph: /me ≡ /users/{signed-in-user}) — both resolve to whatever this header (or the default MCP user) points at. An explicit path email always wins over the header. The unified MCP server does not send this header — it relies on the per-instance default — so it is primarily for direct REST/HTTP callers."}},"schemas":{"ats.Requisition":{"type":"object","properties":{"id":{"type":"string","readOnly":true},"title":{"type":"string","example":"Software Engineer"},"department":{"type":"string","enum":["Engineering","Sales","Marketing","Finance","HR","Product"],"example":"Engineering"},"status":{"type":"string","enum":["open","in_progress","on_hold","closed","filled","cancelled"],"example":"open"},"headcount":{"type":"integer","example":1},"hiringManager":{"type":"string","nullable":true,"example":"alice@example.com","description":"Hiring manager email. Alias: hiring_manager (snake_case accepted)."},"createdBy":{"type":"string","nullable":true,"example":"recruiter@example.com","description":"Email of the user who created this requisition. Alias: created_by (snake_case accepted)."},"responsibilities":{"type":"string","nullable":true,"example":"Design, build, and maintain backend services and APIs; participate in code review and on-call rotations.","description":"Short prose description of what the role does."},"required_skills":{"type":"array","description":"Skills the role requires, each with a target proficiency. Alias: requiredSkills (camelCase accepted).","items":{"type":"object","properties":{"skill":{"type":"string","example":"Python"},"proficiency":{"type":"string","enum":["beginner","intermediate","advanced"],"example":"advanced"}}}},"level":{"type":"string","nullable":true,"example":"Sr","description":"Seniority label — e.g. IC1/IC2/IC3/Sr/Manager/Director, or any free-text level."},"target_role_for":{"type":"array","description":"IDs of requisitions/roles this one is a common next step from (career pathing). Alias: targetRoleFor (camelCase accepted).","items":{"type":"string","example":"REQ-0001"}},"openedAt":{"type":"string","format":"date-time","readOnly":true},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"ats.Candidate":{"type":"object","properties":{"id":{"type":"string","readOnly":true},"firstName":{"type":"string","example":"Jordan","description":"Alias: first_name (snake_case accepted)."},"lastName":{"type":"string","example":"Patel","description":"Alias: last_name (snake_case accepted)."},"email":{"type":"string","format":"email","example":"jordan.patel0@email.com"},"phone":{"type":"string","nullable":true,"example":"+1-415-555-1234"},"requisitionId":{"type":"string","nullable":true,"example":"REQ-0001","description":"Alias: requisition_id (snake_case accepted)."},"status":{"type":"string","enum":["applied","screening","phone_screen","interview","offer","hired","rejected","withdrawn"],"example":"applied","description":"Current pipeline stage. Alias: stage (accepted on create/update)."},"source":{"type":"string","nullable":true,"enum":["LinkedIn","Referral","Indeed","Company Website","Glassdoor"],"example":"LinkedIn"},"resumeUrl":{"type":"string","nullable":true,"example":"https://storage.example.com/resumes/jordan-patel.pdf","description":"Alias: resume_url (snake_case accepted)."},"appliedAt":{"type":"string","format":"date-time","readOnly":true},"stageHistory":{"type":"array","readOnly":true,"items":{"type":"object","properties":{"status":{"type":"string"},"notes":{"type":"string","nullable":true},"recordedAt":{"type":"string","format":"date-time"}}}},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"ats.Interview":{"type":"object","properties":{"id":{"type":"string","readOnly":true},"candidateId":{"type":"string","nullable":true,"example":"CAND-0001","description":"Alias: candidate_id (snake_case accepted)."},"requisitionId":{"type":"string","nullable":true,"example":"REQ-0001","description":"Alias: requisition_id (snake_case accepted)."},"interviewer":{"type":"string","nullable":true,"example":"alice@example.com"},"scheduledAt":{"type":"string","format":"date-time","nullable":true,"example":"2026-06-10T14:00:00.000Z","description":"Alias: scheduled_at (snake_case accepted)."},"type":{"type":"string","nullable":true,"enum":["phone_screen","technical","behavioral","panel","final"],"example":"technical"},"status":{"type":"string","enum":["scheduled","completed","cancelled","no_show"],"example":"scheduled"},"feedback":{"type":"string","nullable":true,"example":"Strong problem-solving skills, good communication."},"rating":{"type":"integer","nullable":true,"minimum":1,"maximum":5,"example":4},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"ats.Offer":{"type":"object","properties":{"id":{"type":"string","readOnly":true},"candidateId":{"type":"string","nullable":true,"example":"CAND-0001","description":"Alias: candidate_id (snake_case accepted)."},"requisitionId":{"type":"string","nullable":true,"example":"REQ-0001","description":"Alias: requisition_id (snake_case accepted)."},"baseSalary":{"type":"number","nullable":true,"example":120000,"description":"Base salary amount. Alias: salary (accepted on create/update)."},"equity":{"type":"string","nullable":true,"example":"0.25%"},"startDate":{"type":"string","format":"date-time","nullable":true,"example":"2026-07-01T00:00:00.000Z","description":"Alias: start_date (snake_case accepted)."},"status":{"type":"string","enum":["draft","sent","accepted","declined","expired","withdrawn"],"example":"draft"},"sentAt":{"type":"string","format":"date-time","nullable":true,"example":"2026-06-01T00:00:00.000Z","description":"Timestamp when the offer was dispatched to the candidate. Client-settable."},"expiresAt":{"type":"string","format":"date-time","nullable":true,"example":"2026-06-15T00:00:00.000Z","description":"Alias: expires_at (snake_case accepted)."},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"ats.JobPosting":{"type":"object","properties":{"id":{"type":"string","readOnly":true},"requisitionId":{"type":"string","nullable":true,"example":"REQ-0001","description":"Alias: requisition_id (snake_case accepted)."},"title":{"type":"string","example":"Software Engineer"},"department":{"type":"string","nullable":true,"example":"Engineering"},"description":{"type":"string","nullable":true,"example":"We are looking for a talented Software Engineer to join our team."},"location":{"type":"string","nullable":true,"example":"Remote"},"type":{"type":"string","nullable":true,"enum":["full_time","part_time","contract","internship"],"example":"full_time"},"status":{"type":"string","enum":["draft","active","paused","closed"],"example":"draft"},"applicantCount":{"type":"integer","nullable":true,"example":42,"description":"Running applicant count. Defaults to 0 on create; may be set by the client and is not server-managed."},"postedAt":{"type":"string","format":"date-time","nullable":true,"example":"2026-06-01T00:00:00.000Z","description":"Alias: posted_at (snake_case accepted)."},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"bank-ops.Branch":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"BR-001"},"country":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"name":{"type":"string","example":"Mumbai Fort Branch"},"city":{"type":"string"},"region":{"type":"string"},"manager":{"type":"string","format":"email"},"status":{"type":"string","enum":["open","closed_today","limited"],"example":"open"},"routing":{"type":"object","properties":{"label":{"type":"string","example":"IFSC"},"code":{"type":"string"}}},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"bank-ops.BranchChecklist":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"CKL-0001"},"branch_id":{"type":"string","example":"BR-001"},"country":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"date":{"type":"string","format":"date"},"items":{"type":"array","items":{"type":"object","properties":{"item_id":{"type":"string"},"label":{"type":"string"},"category":{"type":"string"},"status":{"type":"string","enum":["pending","done","blocked"]},"completed_by":{"type":"string","nullable":true},"completed_at":{"type":"string","format":"date-time","nullable":true}}}},"overall_status":{"type":"string","enum":["pending","complete"],"example":"pending"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"bank-ops.BranchApproval":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"APR-0001"},"branch_id":{"type":"string"},"country":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"type":{"type":"string","enum":["limit_override","fee_waiver","large_withdrawal","loan_exception"],"example":"large_withdrawal"},"requested_by":{"type":"string","format":"email"},"customer_id":{"type":"string","nullable":true},"amount":{"type":"object","properties":{"amount":{"type":"number","example":5000000},"currency":{"type":"string","example":"USD"},"amount_formatted":{"type":"string","example":"$5,000,000"}}},"status":{"type":"string","enum":["pending","approved","rejected"],"example":"pending"},"decision_reason":{"type":"string","nullable":true},"status_history":{"type":"array","readOnly":true,"items":{"type":"object","properties":{"status":{"type":"string"},"note":{"type":"string","nullable":true},"recorded_at":{"type":"string","format":"date-time"}}}},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"bank-ops.Escalation":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"ESC-0001"},"branch_id":{"type":"string"},"country":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"category":{"type":"string","enum":["service","fraud","system_outage","compliance"],"example":"service"},"severity":{"type":"string","enum":["low","medium","high","critical"],"example":"high"},"status":{"type":"string","enum":["open","acknowledged","resolved"],"example":"open"},"owner":{"type":"string","format":"email"},"summary":{"type":"string"},"linked_ref":{"type":"string","nullable":true,"description":"Optional reference to a fraud case / complaint / incident id"},"raised_at":{"type":"string","format":"date-time"},"status_history":{"type":"array","readOnly":true,"items":{"type":"object","properties":{"status":{"type":"string"},"note":{"type":"string","nullable":true},"recorded_at":{"type":"string","format":"date-time"}}}},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"bank-ops.OperationalRisk":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"OPR-0001"},"branch_id":{"type":"string"},"country":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"risk_type":{"type":"string","enum":["cash_shortage","sla_breach","staffing","control_gap"],"example":"cash_shortage"},"severity":{"type":"string","enum":["low","medium","high","critical"],"example":"medium"},"status":{"type":"string","enum":["open","mitigating","closed"],"example":"open"},"due_today":{"type":"boolean","example":true},"mitigation_plan":{"type":"string"},"status_history":{"type":"array","readOnly":true,"items":{"type":"object","properties":{"status":{"type":"string"},"note":{"type":"string","nullable":true},"recorded_at":{"type":"string","format":"date-time"}}}},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"bank-ops.LetterOfCredit":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"LC-0001"},"customer_id":{"type":"string","example":"CUST-0005"},"country":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"applicant":{"type":"string"},"beneficiary":{"type":"string"},"amount":{"type":"object","properties":{"amount":{"type":"number","example":5000000},"currency":{"type":"string","example":"USD"},"amount_formatted":{"type":"string","example":"$5,000,000"}}},"incoterm":{"type":"string","example":"FOB"},"status":{"type":"string","enum":["issued","docs_presented","under_review","accepted","discrepant","settled"],"example":"docs_presented"},"expiry_date":{"type":"string","format":"date"},"milestones":{"type":"array","items":{"type":"object","properties":{"milestone":{"type":"string","enum":["application","issuance","shipment","doc_presentation","payment"]},"status":{"type":"string"},"date":{"type":"string","format":"date-time","nullable":true},"bottleneck_note":{"type":"string","nullable":true}}}},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"bank-ops.BankGuarantee":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"BG-0001"},"customer_id":{"type":"string"},"country":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"type":{"type":"string","enum":["performance","financial","bid_bond"],"example":"performance"},"amount":{"type":"object","properties":{"amount":{"type":"number","example":5000000},"currency":{"type":"string","example":"USD"},"amount_formatted":{"type":"string","example":"$5,000,000"}}},"status":{"type":"string","enum":["active","invoked","expired","released"],"example":"active"},"expiry_date":{"type":"string","format":"date"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"calendar.DateTimeTimeZone":{"type":"object","properties":{"dateTime":{"type":"string","description":"Naive ISO 8601 date-time (no Z suffix when a timeZone is supplied)","example":"2026-06-02T09:00:00.0000000"},"timeZone":{"type":"string","description":"IANA or Windows timezone name","example":"Pacific Standard Time"}},"required":["dateTime","timeZone"]},"calendar.EmailAddress":{"type":"object","properties":{"name":{"type":"string","example":"Alice Chen"},"address":{"type":"string","format":"email","example":"alice.chen@acme.com"}}},"calendar.ItemBody":{"type":"object","properties":{"contentType":{"type":"string","enum":["text","html"],"example":"text"},"content":{"type":"string","example":"15-minute team standup — share blockers and updates."}}},"calendar.Location":{"type":"object","properties":{"displayName":{"type":"string","example":"Sequoia (HQ 2F)"},"locationType":{"type":"string","example":"conferenceRoom"},"uniqueId":{"type":"string"},"uniqueIdType":{"type":"string"}}},"calendar.Attendee":{"type":"object","properties":{"emailAddress":{"$ref":"#/components/schemas/calendar.EmailAddress"},"type":{"type":"string","enum":["required","optional","resource"],"example":"required"},"status":{"type":"object","properties":{"response":{"type":"string","enum":["none","organizer","tentativelyAccepted","accepted","declined","notResponded"],"example":"accepted"},"time":{"type":"string","format":"date-time","example":"2026-05-29T10:00:00.000Z"}}}}},"calendar.PatternedRecurrence":{"type":"object","nullable":true,"properties":{"pattern":{"type":"object","properties":{"type":{"type":"string","enum":["daily","weekly","absoluteMonthly","relativeMonthly","absoluteYearly","relativeYearly"],"example":"weekly"},"interval":{"type":"integer","example":1},"daysOfWeek":{"type":"array","items":{"type":"string"},"example":["monday","tuesday","wednesday","thursday","friday"]},"dayOfMonth":{"type":"integer"},"month":{"type":"integer"},"firstDayOfWeek":{"type":"string","example":"sunday"},"index":{"type":"string"}}},"range":{"type":"object","properties":{"type":{"type":"string","enum":["endDate","noEnd","numbered"],"example":"noEnd"},"startDate":{"type":"string","format":"date","example":"2026-06-02"},"endDate":{"type":"string","format":"date"},"numberOfOccurrences":{"type":"integer"},"recurrenceTimeZone":{"type":"string","example":"Pacific Standard Time"}}}}},"calendar.Event":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"EVT-0001"},"subject":{"type":"string","example":"Daily Standup"},"body":{"$ref":"#/components/schemas/calendar.ItemBody"},"bodyPreview":{"type":"string","readOnly":true,"example":"15-minute team standup — share blockers and updates."},"start":{"$ref":"#/components/schemas/calendar.DateTimeTimeZone"},"end":{"$ref":"#/components/schemas/calendar.DateTimeTimeZone"},"location":{"$ref":"#/components/schemas/calendar.Location"},"locations":{"type":"array","items":{"$ref":"#/components/schemas/calendar.Location"}},"attendees":{"type":"array","items":{"$ref":"#/components/schemas/calendar.Attendee"}},"organizer":{"type":"object","properties":{"emailAddress":{"$ref":"#/components/schemas/calendar.EmailAddress"}}},"isAllDay":{"type":"boolean","example":false},"isCancelled":{"type":"boolean","readOnly":true,"example":false},"isDraft":{"type":"boolean","readOnly":true,"example":false},"isOnlineMeeting":{"type":"boolean","example":true},"isOrganizer":{"type":"boolean","readOnly":true,"example":true},"isReminderOn":{"type":"boolean","example":true},"onlineMeeting":{"type":"object","nullable":true,"properties":{"joinUrl":{"type":"string","example":"https://zoom.us/j/1234567890"},"conferenceId":{"type":"string","nullable":true},"tollNumber":{"type":"string","nullable":true}}},"onlineMeetingProvider":{"type":"string","enum":["unknown","teamsForBusiness","skypeForBusiness","skypeForConsumer"],"example":"teamsForBusiness"},"importance":{"type":"string","enum":["low","normal","high"],"example":"normal"},"sensitivity":{"type":"string","enum":["normal","personal","private","confidential"],"example":"normal"},"showAs":{"type":"string","enum":["free","tentative","busy","oof","workingElsewhere","unknown"],"example":"busy"},"recurrence":{"$ref":"#/components/schemas/calendar.PatternedRecurrence"},"categories":{"type":"array","items":{"type":"string"}},"allowNewTimeProposals":{"type":"boolean","example":true},"hideAttendees":{"type":"boolean","example":false},"reminderMinutesBeforeStart":{"type":"integer","example":15},"responseRequested":{"type":"boolean","example":true},"responseStatus":{"type":"object","properties":{"response":{"type":"string","example":"organizer"},"time":{"type":"string","format":"date-time"}}},"type":{"type":"string","enum":["singleInstance","occurrence","exception","seriesMaster"],"readOnly":true,"example":"singleInstance"},"seriesMasterId":{"type":"string","nullable":true,"readOnly":true,"description":"For occurrences and exceptions, the id of the recurring series master; null otherwise","example":null},"cancellationComment":{"type":"string","readOnly":true,"description":"Cancellation message sent to attendees (set by POST /events/{id}/cancel)","example":"Meeting is no longer needed."},"iCalUId":{"type":"string","readOnly":true,"example":"EVT-0001@purple-suite"},"webLink":{"type":"string","readOnly":true,"example":""},"changeKey":{"type":"string","readOnly":true,"example":""},"hasAttachments":{"type":"boolean","readOnly":true,"example":false},"roomId":{"type":"string","nullable":true,"description":"Purple Suite room id (ROOM-xxx); null if no room booked","example":"ROOM-001"},"calendarId":{"type":"string","description":"Calendar this event belongs to","example":"CAL-001"},"createdDateTime":{"type":"string","format":"date-time","readOnly":true},"lastModifiedDateTime":{"type":"string","format":"date-time","readOnly":true}}},"calendar.Calendar":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"CAL-001"},"name":{"type":"string","example":"Alice Chen"},"color":{"type":"string","enum":["auto","lightBlue","lightGreen","lightOrange","lightGray","lightYellow","lightTeal","lightPink","lightBrown","lightRed","maxColor"],"example":"auto"},"hexColor":{"type":"string","readOnly":true,"example":"#4285F4"},"isDefaultCalendar":{"type":"boolean","example":true},"canEdit":{"type":"boolean","readOnly":true,"example":true},"canShare":{"type":"boolean","readOnly":true,"example":true},"canViewPrivateItems":{"type":"boolean","readOnly":true,"example":true},"allowedOnlineMeetingProviders":{"type":"array","items":{"type":"string"},"example":["teamsForBusiness"]},"defaultOnlineMeetingProvider":{"type":"string","example":"teamsForBusiness"},"isRemovable":{"type":"boolean","example":false},"isTallyingResponses":{"type":"boolean","example":true},"owner":{"$ref":"#/components/schemas/calendar.EmailAddress"},"changeKey":{"type":"string","readOnly":true,"example":""},"createdDateTime":{"type":"string","format":"date-time","readOnly":true},"lastModifiedDateTime":{"type":"string","format":"date-time","readOnly":true}}},"calendar.Room":{"type":"object","properties":{"@odata.type":{"type":"string","default":"#microsoft.graph.room","example":"#microsoft.graph.room"},"id":{"type":"string","readOnly":true,"example":"ROOM-001"},"displayName":{"type":"string","example":"Sequoia"},"emailAddress":{"type":"string","format":"email","example":"sequoia-2f@acme.com"},"capacity":{"type":"integer","example":8},"building":{"type":"string","example":"HQ"},"floorNumber":{"type":"integer","example":2},"floorLabel":{"type":"string","example":"2"},"nickname":{"type":"string","example":"sequoia"},"label":{"type":"string","example":"Sequoia (HQ 2F)"},"bookingType":{"type":"string","enum":["unknown","standard","reserved"],"example":"standard"},"isWheelChairAccessible":{"type":"boolean","example":false},"phone":{"type":"string","nullable":true,"example":null},"tags":{"type":"array","items":{"type":"string"},"description":"Equipment tags (e.g. whiteboard, tv, video_conferencing, phone)","example":["whiteboard","tv","video_conferencing"]},"audioDeviceName":{"type":"string","nullable":true,"example":"Polycom"},"videoDeviceName":{"type":"string","nullable":true,"example":"Logitech Rally"},"displayDeviceName":{"type":"string","nullable":true,"example":"Samsung Display"},"address":{"type":"object","nullable":true,"properties":{"street":{"type":"string"},"city":{"type":"string"},"state":{"type":"string"},"postalCode":{"type":"string"},"countryOrRegion":{"type":"string"}}},"geoCoordinates":{"type":"object","nullable":true},"createdDateTime":{"type":"string","format":"date-time","readOnly":true},"lastModifiedDateTime":{"type":"string","format":"date-time","readOnly":true}}},"calendar.DistributionList":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"DL-001"},"name":{"type":"string","example":"Engineering All"},"email":{"type":"string","format":"email","example":"engineering-all@acme.com"},"description":{"type":"string","example":"All engineering team members"},"type":{"type":"string","enum":["static","dynamic"],"example":"static"},"members":{"type":"array","items":{"type":"string","format":"email"},"description":"Member email addresses","example":["alice.chen@acme.com","bob.martinez@acme.com"]},"memberCount":{"type":"integer","readOnly":true,"example":2,"description":"Maintained automatically when members are added/removed"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"calendar.UserSettings":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"USET-001"},"email":{"type":"string","format":"email","example":"alice.chen@acme.com"},"displayName":{"type":"string","example":"Alice Chen"},"title":{"type":"string","example":"Engineering Manager"},"avatarColor":{"type":"string","description":"Hex color used for the user avatar in the UI","example":"#4285F4"},"timeZone":{"type":"string","description":"IANA timezone string","example":"America/Los_Angeles"},"dateFormat":{"type":"string","example":"MM/dd/yyyy"},"timeFormat":{"type":"string","example":"h:mm tt"},"language":{"type":"object","properties":{"locale":{"type":"string","example":"en-US"},"displayName":{"type":"string","example":"English (United States)"}}},"workingHours":{"type":"object","properties":{"daysOfWeek":{"type":"array","items":{"type":"string"},"example":["monday","tuesday","wednesday","thursday","friday"]},"startTime":{"type":"string","description":"HH:MM:SS.0000000 format","example":"08:00:00.0000000"},"endTime":{"type":"string","description":"HH:MM:SS.0000000 format","example":"17:00:00.0000000"},"timeZone":{"type":"object","properties":{"name":{"type":"string","example":"America/Los_Angeles"}}}}},"automaticRepliesSetting":{"type":"object","properties":{"status":{"type":"string","enum":["disabled","alwaysEnabled","scheduled"],"example":"disabled"},"externalAudience":{"type":"string","enum":["none","contactsOnly","all"],"example":"all"},"internalReplyMessage":{"type":"string","example":""},"externalReplyMessage":{"type":"string","example":""}}},"delegateMeetingMessageDeliveryOptions":{"type":"string","example":"sendToDelegateOnly"},"userPurpose":{"type":"string","readOnly":true,"example":"user"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"calendar.MailboxSettings":{"type":"object","description":"Graph-compatible mailbox settings projection of UserSettings. GET/PATCH via /users/{email}/mailboxSettings.","properties":{"timeZone":{"type":"string","example":"America/Los_Angeles"},"dateFormat":{"type":"string","example":"MM/dd/yyyy"},"timeFormat":{"type":"string","example":"h:mm tt"},"language":{"type":"object","properties":{"locale":{"type":"string","example":"en-US"},"displayName":{"type":"string","example":"English (United States)"}}},"workingHours":{"type":"object","properties":{"daysOfWeek":{"type":"array","items":{"type":"string"},"example":["monday","tuesday","wednesday","thursday","friday"]},"startTime":{"type":"string","example":"08:00:00.0000000"},"endTime":{"type":"string","example":"17:00:00.0000000"},"timeZone":{"type":"object","properties":{"name":{"type":"string","example":"America/Los_Angeles"}}}}},"automaticRepliesSetting":{"type":"object","properties":{"status":{"type":"string","enum":["disabled","alwaysEnabled","scheduled"],"example":"disabled"},"externalAudience":{"type":"string","enum":["none","contactsOnly","all"],"example":"all"},"internalReplyMessage":{"type":"string","example":""},"externalReplyMessage":{"type":"string","example":""}}},"delegateMeetingMessageDeliveryOptions":{"type":"string","example":"sendToDelegateOnly"},"userPurpose":{"type":"string","readOnly":true,"example":"user"}}},"calendar.ScheduleInformation":{"type":"object","properties":{"scheduleId":{"type":"string","format":"email","example":"alice.chen@acme.com"},"availabilityView":{"type":"string","description":"Encoded string: 0=free, 1=tentative, 2=busy, 3=oof, 4=workingElsewhere","example":"0020000"},"scheduleItems":{"type":"array","items":{"type":"object","properties":{"status":{"type":"string","example":"busy"},"subject":{"type":"string","example":"Daily Standup"},"location":{"type":"string","example":""},"start":{"$ref":"#/components/schemas/calendar.DateTimeTimeZone"},"end":{"$ref":"#/components/schemas/calendar.DateTimeTimeZone"},"isPrivate":{"type":"boolean","example":false}}}},"workingHours":{"type":"object","properties":{"daysOfWeek":{"type":"array","items":{"type":"string"},"example":["monday","tuesday","wednesday","thursday","friday"]},"startTime":{"type":"string","example":"08:00:00.0000000"},"endTime":{"type":"string","example":"17:00:00.0000000"},"timeZone":{"type":"object","properties":{"name":{"type":"string","example":"America/Los_Angeles"}}}}}}},"chat.Channel":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"CH-001"},"name":{"type":"string","example":"engineering"},"type":{"type":"string","enum":["public","private"],"example":"public"},"description":{"type":"string","example":"Engineering team discussions, PRs, and deploys"},"memberCount":{"type":"integer","example":12},"members":{"type":"array","items":{"type":"string"},"example":["USR-0001","USR-0002"],"description":"Array of user IDs who are members of this channel"},"createdBy":{"type":"string","example":"alice.chen@acme.com"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"chat.Message":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"MSG-00001"},"channelId":{"type":"string","example":"CH-002","description":"ID of the channel this message belongs to"},"author":{"type":"string","example":"bob.martinez@acme.com"},"text":{"type":"string","example":"Merged the auth refactor PR — please pull latest."},"reactions":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}},"example":{"👍":["alice.chen@acme.com","carol.johnson@acme.com"],"🚀":["dave.williams@acme.com"]},"description":"Map of emoji to list of user emails who reacted"},"editedAt":{"type":"string","format":"date-time","nullable":true,"example":null},"parent_message_id":{"type":"string","nullable":true,"example":null,"description":"Set when this message is a thread reply; populated automatically by POST /messages/:id/reply"},"thread_count":{"type":"integer","example":0,"readOnly":true,"description":"Number of replies in this message's thread; incremented automatically by POST /messages/:id/reply"},"interactive_state":{"type":"object","additionalProperties":{},"readOnly":true,"description":"Current state of interactive button values keyed by action_id; managed by POST /messages/:id/interact","example":{"approve_button":"approved"}},"interactions":{"type":"array","readOnly":true,"items":{"type":"object","properties":{"action_id":{"type":"string"},"value":{},"timestamp":{"type":"string","format":"date-time"}}},"description":"Append-only log of all interactions on this message; managed by POST /messages/:id/interact"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"chat.User":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"USR-0001"},"displayName":{"type":"string","example":"Alice Chen"},"email":{"type":"string","format":"email","example":"alice.chen@acme.com"},"status":{"type":"string","enum":["active","away","offline","do_not_disturb"],"example":"active"},"role":{"type":"string","enum":["admin","member","guest"],"example":"member"},"timezone":{"type":"string","example":"America/Los_Angeles"},"joinedAt":{"type":"string","format":"date-time","example":"2025-05-29T00:00:00.000Z"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"chat.File":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"FILE-0001"},"name":{"type":"string","example":"Q1-report.pdf"},"mimeType":{"type":"string","example":"application/pdf"},"size":{"type":"integer","description":"File size in bytes","example":204800},"url":{"type":"string","format":"uri","example":"https://storage.example.com/files/FILE-0001/Q1-report.pdf"},"uploadedBy":{"type":"string","example":"alice.chen@acme.com"},"channelId":{"type":"string","nullable":true,"example":"CH-001","description":"Channel the file was shared in, if applicable"},"messageId":{"type":"string","nullable":true,"example":"MSG-00001","description":"Message the file is attached to, if applicable"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"chat.ScheduledMessage":{"type":"object","description":"A message queued to be delivered to a channel at a future time (Slack-style scheduled message).","properties":{"id":{"type":"string","readOnly":true,"example":"SCHED-0001"},"channelId":{"type":"string","example":"CH-002","description":"ID of the channel the message will be posted to"},"author":{"type":"string","format":"email","example":"bob.martinez@acme.com"},"text":{"type":"string","example":"Reminder: standup in 10 minutes."},"scheduledFor":{"type":"string","format":"date-time","example":"2026-04-02T09:00:00.000Z","description":"When the message is scheduled to be sent"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"chat.Draft":{"type":"object","description":"An unsent message draft (Slack-style draft). Saving a draft does NOT deliver the message.","properties":{"id":{"type":"string","readOnly":true,"example":"DRAFT-0001"},"channelId":{"type":"string","nullable":true,"example":"CH-002","description":"Target channel, when the draft is for a channel"},"recipient":{"type":"string","format":"email","nullable":true,"example":"ajay.patel@acme.com","description":"Target user email, when the draft is a direct message"},"author":{"type":"string","format":"email","example":"bob.martinez@acme.com"},"text":{"type":"string","example":"Hey Ajay — draft of the launch note, not sent yet."},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"community.FeatureRequest":{"type":"object","properties":{"id":{"type":"string","readOnly":true},"name":{"type":"string","example":"Add Dark Mode"},"currentStatus":{"type":"string","enum":["New","Under Review","Planned","In Progress","Delivered","Declined"],"example":"New"},"createdBy":{"type":"string","example":"user1@acme.com"},"moderator":{"type":"string","example":"admin@acme.com"},"productArea":{"type":"string","enum":["UI/UX","Performance","Security","Integrations","API","Analytics","Admin","Notifications"],"example":"UI/UX"},"votes":{"type":"integer","example":42},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"community.LegacyFeatureRequest":{"type":"object","properties":{"id":{"type":"string","example":"abc123","description":"Unique identifier of the feature request within the session"},"current_status":{"type":"string","enum":["New","Existing Functionality/Native Skill","Future Consideration","Not Enough Info to Make Decision","On Roadmap","Delivered","Duplicate","Limited Preview"],"example":"New","description":"Human-readable status mapped from newStatusNumber"}},"additionalProperties":true},"community.UpdateFeatureRequestStatusBody":{"type":"object","required":["newStatusNumber"],"properties":{"newStatusNumber":{"type":"integer","enum":[1,2,3,6,7,8,9,11],"example":7,"description":"Status code to set. Mapping: 1=New, 2=Existing Functionality/Native Skill, 3=Future Consideration, 6=Not Enough Info to Make Decision, 7=On Roadmap, 8=Delivered, 9=Duplicate, 11=Limited Preview"}}},"crm.Account":{"type":"object","properties":{"id":{"type":"string","readOnly":true},"name":{"type":"string","example":"Global Solutions"},"industry":{"type":"string","enum":["Technology","Healthcare","Finance","Retail","Manufacturing","Education","Government","Media"],"example":"Technology"},"territory":{"type":"string","enum":["North America","EMEA","APAC","LATAM"],"example":"North America"},"status":{"type":"string","enum":["customer","prospect","partner","inactive"],"example":"customer"},"owner":{"type":"string","example":"alice@example.com"},"employees":{"type":"integer","example":500},"annualRecurringRevenue":{"type":"number","example":1200000},"monthlyRecurringRevenue":{"type":"number","example":100000},"averageDealSize":{"type":"number","example":50000},"totalOpportunities":{"type":"integer","example":12},"openOpportunities":{"type":"integer","example":4},"closedWonOpportunities":{"type":"integer","example":6},"closedLostOpportunities":{"type":"integer","example":2},"churnRate":{"type":"number","example":3.5},"netPromoterScore":{"type":"integer","example":45},"supportTickets":{"type":"integer","example":7},"officeCount":{"type":"integer","example":3},"marketSharePct":{"type":"number","example":12.5},"growthRate":{"type":"number","example":18},"fundingAmount":{"type":"number","example":10000000},"lastContactDate":{"type":"string","format":"date-time","example":"2026-05-01T00:00:00.000Z"},"nextRenewalDate":{"type":"string","format":"date-time","example":"2026-11-25T00:00:00.000Z"},"lastModifiedAt":{"type":"string","format":"date-time","example":"2026-05-15T00:00:00.000Z"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"crm.Contact":{"type":"object","properties":{"id":{"type":"string","readOnly":true},"firstName":{"type":"string","example":"James"},"lastName":{"type":"string","example":"Chen"},"email":{"type":"string","format":"email","example":"james.chen@example.com"},"title":{"type":"string","enum":["CEO","CTO","VP Sales","Director","Manager","Senior Manager","Engineer","Analyst"],"example":"VP Sales"},"accountId":{"type":"string","example":"ACC-0001"},"phone":{"type":"string","example":"+1-415-555-1234"},"status":{"type":"string","enum":["active","inactive"],"example":"active"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"crm.Opportunity":{"type":"object","properties":{"id":{"type":"string","readOnly":true},"name":{"type":"string","example":"Enterprise New Business - Q2 2024"},"accountId":{"type":"string","example":"ACC-0001"},"stage":{"type":"string","enum":["prospecting","qualification","proposal","negotiation","closed_won","closed_lost"],"example":"qualification"},"amount":{"type":"number","example":75000},"probability":{"type":"integer","example":50},"closeDate":{"type":"string","format":"date-time","example":"2026-06-28T00:00:00.000Z"},"owner":{"type":"string","example":"alice@example.com"},"stageHistory":{"type":"array","readOnly":true,"items":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"},"notes":{"type":"string","nullable":true},"changedAt":{"type":"string","format":"date-time"}}},"description":"Populated automatically by /advance-stage"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"crm.Lead":{"type":"object","properties":{"id":{"type":"string","readOnly":true},"firstName":{"type":"string","example":"Sarah"},"lastName":{"type":"string","example":"Rodriguez"},"company":{"type":"string","example":"TechCo"},"email":{"type":"string","format":"email","example":"lead0@prospect.com"},"phone":{"type":"string","example":"+1-415-555-9876","description":"Optional phone number. If present, copied to the Contact created by /convert."},"title":{"type":"string","example":"VP Engineering","description":"Optional job title. If present, copied to the Contact created by /convert."},"accountId":{"type":"string","example":"ACC-0001","description":"Optional linked account ID. If present, copied to the Contact created by /convert."},"status":{"type":"string","enum":["new","contacted","qualified","converted","disqualified","dead"],"example":"new"},"source":{"type":"string","enum":["Web Form","Trade Show","Referral","Cold Call","LinkedIn","Email Campaign"],"example":"Web Form"},"score":{"type":"integer","example":72},"convertedTo":{"type":"object","readOnly":true,"nullable":true,"properties":{"contactId":{"type":"string"},"opportunityId":{"type":"string","nullable":true}},"description":"Set automatically by /convert"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"crm.Case":{"type":"object","properties":{"id":{"type":"string","readOnly":true},"subject":{"type":"string","example":"Technical support"},"accountId":{"type":"string","example":"ACC-0001"},"status":{"type":"string","enum":["open","in_progress","waiting_on_customer","resolved","closed"],"example":"open"},"priority":{"type":"string","enum":["low","medium","high","critical"],"example":"medium"},"assignee":{"type":"string","example":"alice@example.com"},"resolvedAt":{"type":"string","format":"date-time","nullable":true,"example":null},"reopenReason":{"type":"string","example":"Customer reported issue recurred","description":"Required when reopening a closed case (stored field name). See also: reopen_reason."},"reopen_reason":{"type":"string","example":"Customer reported issue recurred","description":"Input alias for reopenReason accepted by PATCH /cases/:id. Normalised to reopenReason before storage. Use reopenReason in responses."},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"crm.Activity":{"type":"object","properties":{"id":{"type":"string","readOnly":true},"type":{"type":"string","enum":["call","email","meeting","task","note"],"example":"call"},"subject":{"type":"string","example":"Follow-up call"},"accountId":{"type":"string","example":"ACC-0001"},"owner":{"type":"string","example":"alice@example.com"},"completed":{"type":"boolean","example":false},"dueDate":{"type":"string","format":"date-time","example":"2026-06-05T00:00:00.000Z"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"dex.Device":{"type":"object","properties":{"id":{"type":"string","readOnly":true},"hostname":{"type":"string","example":"eng-dell-001"},"device_type":{"type":"string","enum":["laptop","desktop","mobile","virtual_machine"],"example":"laptop"},"os":{"type":"string","example":"macOS 15 Sequoia"},"os_version":{"type":"string","example":"24.3.0"},"manufacturer":{"type":"string","example":"Apple"},"model":{"type":"string","example":"MacBook Pro 16-inch"},"serial_number":{"type":"string","example":"SN-A1B2C3D4"},"assigned_to":{"type":"string","description":"Email of the assigned user","example":"alice@acme.com"},"department":{"type":"string","example":"Engineering"},"location":{"type":"string","example":"San Francisco HQ"},"ip_address":{"type":"string","example":"10.0.1.42"},"last_seen":{"type":"string","format":"date-time","example":"2026-05-28T14:23:00.000Z"},"health_score":{"type":"number","minimum":0,"maximum":100,"example":87},"cpu_usage_pct":{"type":"number","example":34},"memory_usage_pct":{"type":"number","example":61},"disk_usage_pct":{"type":"number","example":45},"battery_health_pct":{"type":"number","example":92},"uptime_hours":{"type":"number","example":168},"compliance_status":{"type":"string","enum":["compliant","non_compliant","unknown"],"example":"compliant"},"encryption_enabled":{"type":"boolean","example":true},"antivirus_status":{"type":"string","enum":["active","outdated","disabled","not_installed"],"example":"active"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"dex.SoftwareInventory":{"type":"object","properties":{"id":{"type":"string","readOnly":true},"device_id":{"type":"string","example":"DEV-0001"},"software_name":{"type":"string","example":"Microsoft 365"},"version":{"type":"string","example":"16.4.0"},"publisher":{"type":"string","example":"Microsoft"},"install_date":{"type":"string","format":"date","example":"2025-11-15"},"last_used":{"type":"string","format":"date-time","example":"2026-05-27T09:00:00.000Z"},"license_type":{"type":"string","enum":["licensed","freeware","open_source","trial","unknown"],"example":"licensed"},"is_managed":{"type":"boolean","example":true},"is_approved":{"type":"boolean","example":true},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"dex.DexAlert":{"type":"object","properties":{"id":{"type":"string","readOnly":true},"device_id":{"type":"string","example":"DEV-0003"},"alert_type":{"type":"string","enum":["high_cpu","low_disk","low_memory","crash","network_issue","battery_critical","security_threat","compliance_violation","software_error","hardware_failure"],"example":"high_cpu"},"severity":{"type":"string","enum":["critical","high","medium","low","info"],"example":"high"},"status":{"type":"string","enum":["open","acknowledged","resolved"],"example":"open"},"title":{"type":"string","example":"CPU sustained above 95% for 30 min"},"description":{"type":"string","example":"Automated alert triggered on endpoint device."},"metric_value":{"type":"number","description":"The metric reading that triggered the alert","example":97},"threshold":{"type":"number","description":"The threshold that was exceeded","example":90},"acknowledged":{"type":"boolean","example":false},"acknowledged_by":{"type":"string","example":"admin@acme.com"},"acknowledged_at":{"type":"string","format":"date-time","example":"2026-05-28T10:00:00.000Z"},"resolved_at":{"type":"string","format":"date-time","example":"2026-05-28T11:30:00.000Z"},"resolved_by":{"type":"string","example":"admin@acme.com"},"resolution_notes":{"type":"string","description":"Notes recorded when the alert was resolved","example":"Restarted service, CPU normalized."},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"dex.Remediation":{"type":"object","properties":{"id":{"type":"string","readOnly":true},"device_id":{"type":"string","example":"DEV-0003"},"action_type":{"type":"string","enum":["restart_service","clear_cache","force_update","restart_device","run_diagnostics","repair_installation","reset_network","update_drivers","scan_malware","enforce_compliance"],"example":"clear_cache"},"status":{"type":"string","enum":["pending","in_progress","completed","failed","cancelled"],"example":"pending"},"target_software":{"type":"string","description":"Optional: specific software targeted","example":"Microsoft Teams"},"executed_by":{"type":"string","example":"admin@acme.com"},"executed_at":{"type":"string","format":"date-time","example":"2026-05-28T12:00:00.000Z"},"completed_at":{"type":"string","format":"date-time","example":"2026-05-28T12:05:00.000Z"},"result":{"type":"string","example":"success"},"notes":{"type":"string","example":"Auto-triggered by alert"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"dex.ExperienceScore":{"type":"object","properties":{"id":{"type":"string","readOnly":true},"device_id":{"type":"string","example":"DEV-0001"},"user_email":{"type":"string","example":"alice@acme.com"},"overall_score":{"type":"number","minimum":0,"maximum":10,"example":7.8},"boot_time_score":{"type":"number","minimum":0,"maximum":10,"example":8.2},"responsiveness_score":{"type":"number","minimum":0,"maximum":10,"example":7.5},"network_score":{"type":"number","minimum":0,"maximum":10,"example":8},"stability_score":{"type":"number","minimum":0,"maximum":10,"example":7.1},"battery_score":{"type":"number","minimum":0,"maximum":10,"example":8.6},"measured_at":{"type":"string","format":"date-time","example":"2026-05-29T06:00:00.000Z"},"boot_time_seconds":{"type":"number","example":28},"avg_app_crash_count":{"type":"number","example":1},"avg_response_time_ms":{"type":"number","example":320},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"dex.Campaign":{"type":"object","properties":{"id":{"type":"string","readOnly":true},"name":{"type":"string","example":"Q2 2026 patch deployment"},"description":{"type":"string","example":"Automated patch deployment campaign targeting managed devices."},"campaign_type":{"type":"string","enum":["patch_deployment","software_rollout","compliance_check","security_scan","driver_update","os_upgrade"],"example":"patch_deployment"},"status":{"type":"string","enum":["draft","active","paused","completed","cancelled"],"example":"draft"},"target_criteria":{"type":"object","description":"Filter criteria for targeted devices (e.g. OS, department)","example":{"os":"Windows 11","department":"Engineering"}},"total_devices":{"type":"integer","example":20},"completed_devices":{"type":"integer","example":0},"failed_devices":{"type":"integer","example":0},"launched_at":{"type":"string","format":"date-time","example":"2026-05-29T08:00:00.000Z"},"launched_by":{"type":"string","example":"admin@acme.com"},"created_by":{"type":"string","example":"admin@acme.com"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"erp.PurchaseOrder":{"type":"object","properties":{"id":{"type":"string","readOnly":true},"vendor":{"type":"string","example":"Acme Corp"},"status":{"type":"string","enum":["draft","pending_approval","approved","sent","partially_received","received","cancelled"],"example":"draft"},"totalAmount":{"type":"number","example":12500},"currency":{"type":"string","example":"USD"},"lineItems":{"type":"integer","example":3},"approver":{"type":"string","example":"finance@acme.com"},"expectedDelivery":{"type":"string","format":"date-time","example":"2026-06-15T00:00:00.000Z"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"erp.Invoice":{"type":"object","properties":{"id":{"type":"string","readOnly":true},"vendor":{"type":"string","example":"Acme Corp"},"status":{"type":"string","enum":["draft","pending_payment","paid","void","overdue"],"example":"draft"},"amount":{"type":"number","example":4200},"currency":{"type":"string","example":"USD"},"dueDate":{"type":"string","format":"date-time","example":"2026-06-30T00:00:00.000Z"},"poId":{"type":"string","description":"Purchase order this invoice is linked to (used for 3-way match)","example":"PO-0001"},"notes":{"type":"string","description":"Free-text notes; the only field writable on a voided invoice","example":"Approved by finance"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"erp.InventoryItem":{"type":"object","properties":{"id":{"type":"string","readOnly":true},"itemName":{"type":"string","example":"Laptop Pro 2024"},"sku":{"type":"string","example":"SKU-1A2B3C4D"},"quantity":{"type":"integer","example":50},"reorderPoint":{"type":"integer","example":10},"unitCost":{"type":"number","example":1200},"warehouseLocation":{"type":"string","example":"WH-EAST"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"erp.JournalEntry":{"type":"object","properties":{"id":{"type":"string","readOnly":true},"type":{"type":"string","enum":["debit","credit","adjustment","accrual","reversal"],"example":"accrual"},"debitAccount":{"type":"string","example":"1000-Cash"},"creditAccount":{"type":"string","example":"2000-AP"},"amount":{"type":"number","example":5000},"description":{"type":"string","example":"Monthly accrual"},"status":{"type":"string","enum":["draft","posted","reversed"],"example":"draft"},"period":{"type":"string","example":"2026-06"},"postedAt":{"type":"string","format":"date-time","description":"ISO timestamp when the entry was posted; set automatically on posted and reversal entries","readOnly":true},"reversalOf":{"type":"string","description":"ID of the original entry this reversal was created from","readOnly":true},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"erp.Asset":{"type":"object","properties":{"id":{"type":"string","readOnly":true},"name":{"type":"string","example":"MacBook Pro"},"type":{"type":"string","enum":["IT Equipment","Furniture","Vehicle","Machinery","Building","Software License"],"example":"IT Equipment"},"status":{"type":"string","enum":["active","maintenance","retired","disposed"],"example":"active"},"purchaseValue":{"type":"number","example":2500},"currentValue":{"type":"number","example":1800},"purchasedAt":{"type":"string","format":"date-time","description":"Date the asset was originally purchased","example":"2025-01-15T00:00:00.000Z"},"location":{"type":"string","example":"HQ"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"expense.Trip":{"type":"object","properties":{"id":{"type":"string","readOnly":true},"name":{"type":"string","example":"SFO Customer Visit","description":"Short label for the trip (e.g. 'SF Sales Summit 2026')"},"purpose":{"type":"string","example":"Q2 customer kickoff and partner meetings","description":"Business reason for the trip"},"traveler_email":{"type":"string","example":"alice@acme.com"},"start_date":{"type":"string","format":"date","example":"2026-06-10"},"end_date":{"type":"string","format":"date","example":"2026-06-13"},"origin":{"type":"string","example":"JFK","description":"Departure airport or city code"},"destination":{"type":"string","example":"SFO","description":"Primary destination airport or city code"},"destination_country":{"type":"string","example":"US","description":"ISO country of the destination (derived from the destination airport/city). Cross-reference with HRIS work authorizations to assess travel eligibility."},"is_international":{"type":"boolean","example":true,"description":"True when the destination country differs from the traveler's home country."},"status":{"type":"string","enum":["planned","approved","in_progress","completed","cancelled"],"example":"planned"},"estimated_cost":{"type":"number","example":2400},"actual_cost":{"type":"number","example":2185,"description":"Populated once the trip is in_progress or completed"},"currency":{"type":"string","example":"USD","default":"USD"},"cost_center":{"type":"string","example":"SALES-001"},"project_code":{"type":"string","example":"FY26-Q2-KICKOFF"},"approver_email":{"type":"string","example":"manager@acme.com"},"approved_by":{"type":"string","example":"manager@acme.com","readOnly":true},"approved_at":{"type":"string","format":"date-time","example":"2026-05-20T14:30:00.000Z","readOnly":true},"approval_notes":{"type":"string","example":"Within policy. Approved.","readOnly":true},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"expense.Booking":{"type":"object","properties":{"id":{"type":"string","readOnly":true},"trip_id":{"type":"string","example":"TRIP-0001"},"booking_type":{"type":"string","enum":["flight","hotel","car","rail"],"example":"flight"},"flight_id":{"type":"string","example":"FLT-0001","description":"Set when this booking was made from a flight in the searchable inventory (see POST /flights/{id}/book)"},"hotel_id":{"type":"string","example":"HOT-0001","description":"Set when this booking was made from a hotel in the searchable inventory (see POST /hotels/{id}/book)"},"vendor":{"type":"string","example":"United","description":"Airline, hotel chain, car rental agency, or rail operator"},"confirmation_number":{"type":"string","example":"X4K9PQ"},"status":{"type":"string","enum":["confirmed","cancelled","completed"],"example":"confirmed"},"start_datetime":{"type":"string","format":"date-time","example":"2026-06-10T08:00:00Z"},"end_datetime":{"type":"string","format":"date-time","example":"2026-06-10T11:30:00Z"},"origin":{"type":"string","example":"JFK","description":"Departure airport/city/pickup location (flights only)"},"destination":{"type":"string","example":"SFO","description":"Arrival airport/city/dropoff location"},"fare_class":{"type":"string","example":"Economy","description":"Cabin class (Economy, Business) or room type (Standard King)"},"cost":{"type":"number","example":620},"currency":{"type":"string","example":"USD","default":"USD"},"traveler_email":{"type":"string","example":"alice@acme.com"},"notes":{"type":"string","example":""},"cancelled_at":{"type":"string","format":"date-time","readOnly":true},"cancelled_by":{"type":"string","readOnly":true},"cancellation_reason":{"type":"string","readOnly":true},"refund_amount":{"type":"number","readOnly":true},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"expense.ExpenseReport":{"type":"object","properties":{"id":{"type":"string","readOnly":true},"name":{"type":"string","example":"Customer trip — May","description":"User-facing title (e.g. 'May client trip')"},"employee_email":{"type":"string","example":"alice@acme.com"},"trip_id":{"type":"string","example":"TRIP-0001","description":"Optional link to a Trip"},"status":{"type":"string","enum":["draft","submitted","approved","rejected","reimbursed"],"example":"draft"},"total_amount":{"type":"number","example":1842.5,"description":"Server-maintained running total of attached expense items; recomputed on every item create/update/delete. Values sent by clients are ignored","readOnly":true},"currency":{"type":"string","example":"USD","default":"USD"},"cost_center":{"type":"string","example":"SALES-001"},"project_code":{"type":"string","example":"FY26-Q2-KICKOFF"},"approver_email":{"type":"string","example":"manager@acme.com"},"submitted_at":{"type":"string","format":"date-time","readOnly":true},"submitted_by":{"type":"string","readOnly":true},"approved_at":{"type":"string","format":"date-time","readOnly":true},"approved_by":{"type":"string","readOnly":true},"approver_notes":{"type":"string","readOnly":true},"rejected_at":{"type":"string","format":"date-time","readOnly":true},"rejected_by":{"type":"string","readOnly":true},"rejection_reason":{"type":"string","readOnly":true},"reimbursed_at":{"type":"string","format":"date-time","readOnly":true},"reimbursement_method":{"type":"string","readOnly":true},"reimbursement_reference":{"type":"string","readOnly":true},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"expense.ExpenseItem":{"type":"object","properties":{"id":{"type":"string","readOnly":true},"report_id":{"type":"string","example":"RPT-0001"},"trip_id":{"type":"string","example":"TRIP-0001"},"category":{"type":"string","enum":["airfare","lodging","ground_transportation","meals","entertainment","mileage","parking","tolls","phone","wifi","office_supplies","conference","training","other"],"example":"meals"},"merchant":{"type":"string","example":"Sweetgreen"},"description":{"type":"string","example":"Team dinner"},"transaction_date":{"type":"string","format":"date","example":"2026-06-11"},"amount":{"type":"number","example":87.5},"currency":{"type":"string","example":"USD","default":"USD"},"exchange_rate":{"type":"number","example":1,"description":"Rate to base currency (USD); 1.0 for domestic"},"amount_in_base_currency":{"type":"number","example":87.5},"payment_method":{"type":"string","enum":["corporate_card","personal_card","cash","out_of_pocket"],"example":"corporate_card"},"receipt_url":{"type":"string","example":"https://receipts.purple-suite.example/abc123.pdf","description":"Link to stored receipt image or PDF"},"status":{"type":"string","enum":["pending","approved","rejected","flagged_policy"],"example":"pending"},"policy_violation":{"type":"string","example":"Exceeded daily meal cap","description":"Set when status is flagged_policy"},"is_billable":{"type":"boolean","example":false,"description":"Whether to bill back to a client"},"billable_client":{"type":"string","example":"Acme Corp"},"notes":{"type":"string","example":""},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"expense.ExpensePolicy":{"type":"object","properties":{"id":{"type":"string","readOnly":true},"category":{"type":"string","enum":["airfare","lodging","ground_transportation","meals","entertainment","mileage","parking","tolls","phone","wifi","office_supplies","conference","training","other"],"example":"meals"},"daily_limit":{"type":"number","example":90,"description":"Maximum spend per day for this category (null if not applicable)"},"per_transaction_limit":{"type":"number","example":500,"description":"Maximum per single transaction (null if not applicable)"},"requires_receipt_above":{"type":"number","example":25,"description":"Dollar threshold above which a receipt is required"},"requires_pre_approval_above":{"type":"number","example":500,"description":"Dollar threshold above which pre-approval is required (null if not applicable)"},"allowed_payment_methods":{"type":"array","items":{"type":"string","enum":["corporate_card","personal_card","cash","out_of_pocket"]},"example":["corporate_card","personal_card","out_of_pocket"]},"mileage_rate":{"type":"number","example":0.67,"description":"Per-distance reimbursement rate in the instance's base currency (e.g. IRS $0.67/mi for a US-primary instance, ₹12/km for IN); only relevant for category=mileage"},"mileage_unit":{"type":"string","enum":["km","mi"],"example":"mi","description":"Distance unit the mileage_rate is per; set by the primary locale pack. Only present for category=mileage"},"currency":{"type":"string","example":"USD","default":"USD"},"effective_date":{"type":"string","format":"date","example":"2026-01-01"},"notes":{"type":"string","example":""},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"expense.Approval":{"type":"object","properties":{"id":{"type":"string","readOnly":true},"target_type":{"type":"string","enum":["trip","expense_report"],"example":"expense_report","description":"The kind of record being approved"},"target_id":{"type":"string","example":"RPT-0001"},"requester_email":{"type":"string","example":"alice@acme.com"},"approver_email":{"type":"string","example":"manager@acme.com"},"status":{"type":"string","enum":["pending","approved","rejected"],"example":"pending"},"amount":{"type":"number","example":1842.5},"currency":{"type":"string","example":"USD","default":"USD"},"requested_at":{"type":"string","format":"date-time","example":"2026-05-25T09:00:00.000Z"},"decided_at":{"type":"string","format":"date-time","example":"2026-05-26T11:15:00.000Z"},"decision_notes":{"type":"string","example":"Within policy"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"expense.Airline":{"type":"object","properties":{"id":{"type":"string","readOnly":true},"code":{"type":"string","example":"UA","description":"IATA carrier code"},"name":{"type":"string","example":"United Airlines"},"alliance":{"type":"string","example":"Star Alliance","description":"Airline alliance, or 'None'"},"hubs":{"type":"array","items":{"type":"string"},"example":["SFO","ORD","EWR"],"description":"Primary hub airport codes"},"logo_url":{"type":"string","example":"https://logos.purple-suite.example/ua.png"},"on_time_rate":{"type":"number","example":0.82,"description":"Historical on-time arrival rate (0–1)"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"expense.Flight":{"type":"object","description":"A scheduled flight in the bookable inventory. Search with GET /flights/search, then book with POST /flights/{id}/book.","properties":{"id":{"type":"string","readOnly":true},"flight_number":{"type":"string","example":"UA 523"},"airline_code":{"type":"string","example":"UA"},"airline_name":{"type":"string","example":"United Airlines"},"origin":{"type":"string","example":"JFK","description":"Departure airport code"},"destination":{"type":"string","example":"SFO","description":"Arrival airport code"},"departure_datetime":{"type":"string","format":"date-time","example":"2026-06-10T08:00:00Z"},"arrival_datetime":{"type":"string","format":"date-time","example":"2026-06-10T11:30:00Z"},"duration_minutes":{"type":"integer","example":390},"stops":{"type":"integer","example":0,"description":"Number of layovers (0 = nonstop)"},"aircraft":{"type":"string","example":"Boeing 737-900"},"status":{"type":"string","enum":["scheduled","delayed","cancelled","departed","landed"],"example":"scheduled"},"fares":{"type":"array","description":"Available cabins with price and remaining seats. Booking a cabin decrements its seats_available.","items":{"type":"object","properties":{"cabin":{"type":"string","enum":["Economy","Premium Economy","Business","First"],"example":"Economy"},"price":{"type":"number","example":459},"seats_available":{"type":"integer","example":12},"refundable":{"type":"boolean","example":false}}}},"currency":{"type":"string","example":"USD","default":"USD"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"expense.Hotel":{"type":"object","description":"A bookable hotel in the inventory. Search with GET /hotels/search, then book with POST /hotels/{id}/book.","properties":{"id":{"type":"string","readOnly":true},"name":{"type":"string","example":"Marriott Marquis San Francisco"},"brand":{"type":"string","example":"Marriott"},"city":{"type":"string","example":"San Francisco"},"city_code":{"type":"string","example":"SFO","description":"Airport/city code, aligned with trip destinations"},"address":{"type":"string","example":"780 Mission St, San Francisco, CA"},"star_rating":{"type":"number","example":4,"description":"Star class, 1–5"},"guest_rating":{"type":"number","example":8.6,"description":"Average guest review score, 0–10"},"nightly_rate":{"type":"number","example":289,"description":"Base nightly rate; room_types may override"},"currency":{"type":"string","example":"USD","default":"USD"},"rooms_available":{"type":"integer","example":14,"description":"Bookable rooms remaining; booking decrements this"},"distance_to_city_center_km":{"type":"number","example":1.2},"amenities":{"type":"array","items":{"type":"string"},"example":["WiFi","Gym","Breakfast","Parking"]},"room_types":{"type":"array","description":"Optional room tiers with their own nightly rates","items":{"type":"object","properties":{"name":{"type":"string","example":"Standard King"},"rate":{"type":"number","example":289},"available":{"type":"integer","example":6}}}},"status":{"type":"string","enum":["active","inactive"],"example":"active"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"expense.TravelPolicy":{"type":"object","description":"Fare-class eligibility rule: the minimum seniority a traveler must hold to book a given cabin. Enforced by POST /flights/{id}/book — a traveler below the cabin's min_seniority is rejected with 403. Seniority is resolved from the employee's title (see HRIS).","properties":{"id":{"type":"string","readOnly":true},"cabin_class":{"type":"string","enum":["Economy","Premium Economy","Business","First"],"example":"Business","description":"The flight cabin this rule governs"},"min_seniority":{"type":"string","enum":["Individual Contributor","Manager","Senior Manager","Director","VP"],"example":"VP","description":"Minimum seniority level permitted to book this cabin. Levels, lowest to highest: Individual Contributor, Manager, Senior Manager, Director, VP."},"description":{"type":"string","example":"Business class is reserved for VP level and above."},"effective_date":{"type":"string","format":"date","example":"2026-01-01"},"notes":{"type":"string","example":""},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"expense.PerDiemRate":{"type":"object","description":"Daily travel allowance for a destination: max lodging plus M&IE (meals & incidental expenses), GSA-style. Query the list, or use GET /per_diem_rates/calculate to total it over a number of days.","properties":{"id":{"type":"string","readOnly":true},"city":{"type":"string","example":"San Francisco"},"city_code":{"type":"string","example":"SFO","description":"Airport/city code, aligned with trip destinations and hotel city_code"},"country":{"type":"string","example":"US","description":"ISO country code"},"lodging_rate":{"type":"number","example":325,"description":"Maximum reimbursable lodging per night"},"mie_rate":{"type":"number","example":79,"description":"Meals & incidental expenses allowance per day"},"total_daily_rate":{"type":"number","example":404,"description":"lodging_rate + mie_rate — the full per diem per day"},"currency":{"type":"string","example":"USD","default":"USD","description":"The instance's base currency. Rates for foreign destinations are FX-converted into it at seed time, so they compare directly with trip and expense amounts (which carry the same currency)."},"effective_date":{"type":"string","format":"date","example":"2026-01-01"},"notes":{"type":"string","example":""},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"grid.Base":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"appA1B2C3D4E5F6G7"},"seqId":{"type":"integer","example":1,"description":"Client-supplied display sequence number"},"name":{"type":"string","example":"Product Tracker"},"tableCount":{"type":"integer","example":4},"createdBy":{"type":"string","format":"email","example":"alice@acme.com"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"grid.Table":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"tblH8I9J0K1L2M3N4"},"seqId":{"type":"integer","example":1,"description":"Client-supplied display sequence number"},"baseId":{"type":"string","example":"appA1B2C3D4E5F6G7","description":"ID of the parent base"},"name":{"type":"string","example":"Tasks"},"fieldCount":{"type":"integer","example":8},"recordCount":{"type":"integer","example":150},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"grid.Field":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"fldO5P6Q7R8S9T0U1"},"tableId":{"type":"string","example":"tblH8I9J0K1L2M3N4","description":"ID of the parent table"},"name":{"type":"string","example":"Status"},"type":{"type":"string","enum":["singleLineText","multilineText","number","checkbox","singleSelect","multipleSelect","date","email","url","attachment","linkedRecord","formula","rollup","count","lookup","currency","percent","duration","rating","phoneNumber"],"example":"singleSelect"},"options":{"type":"object","description":"Field-type-specific configuration (e.g. choices for singleSelect)","example":{"choices":[{"name":"Todo"},{"name":"In Progress"},{"name":"Done"}]}},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"grid.Record":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"recV2W3X4Y5Z6A7B8"},"seqId":{"type":"integer","example":1,"description":"Client-supplied display sequence number"},"tableId":{"type":"string","example":"tblH8I9J0K1L2M3N4","description":"ID of the parent table"},"name":{"type":"string","example":"Record Alpha 1"},"status":{"type":"string","enum":["active","inactive","pending","completed","archived"],"example":"active"},"assignee":{"type":"string","format":"email","example":"alice@acme.com"},"fields":{"type":"object","description":"Arbitrary key-value cell data keyed by field name","example":{"Priority":"High","Due Date":"2026-06-30","Notes":"Follow up with team"}},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"grid.View":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"viwC9D0E1F2G3H4I5"},"tableId":{"type":"string","example":"tblH8I9J0K1L2M3N4","description":"ID of the parent table"},"name":{"type":"string","example":"All Tasks"},"type":{"type":"string","enum":["grid","gallery","kanban","calendar","gantt","form"],"example":"grid"},"filterByFormula":{"type":"string","description":"Grid formula used to filter rows in this view","example":"AND({Status}='In Progress', {Assignee}='alice@acme.com')"},"sortField":{"type":"string","example":"Due Date"},"sortDirection":{"type":"string","enum":["asc","desc"],"example":"asc"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"grid.Automation":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"autJ6K7L8M9N0O1P2"},"name":{"type":"string","example":"Notify on Status Change"},"enabled":{"type":"boolean","example":true},"triggerType":{"type":"string","enum":["recordCreated","recordUpdated","recordMatchesConditions","scheduledTime","formSubmitted","webhookReceived"],"example":"recordUpdated"},"triggerField":{"type":"string","description":"Field name that triggers the automation (for recordUpdated triggers)","example":"Status"},"actionType":{"type":"string","enum":["sendEmail","createRecord","updateRecord","sendSlackMessage","runScript","createPage"],"example":"sendEmail"},"actionConfig":{"type":"object","description":"Action-specific configuration payload","example":{"to":"team@acme.com","subject":"Record updated","body":"A record was just updated."}},"runCount":{"type":"integer","example":42,"description":"Number of times this automation has run"},"lastRunAt":{"type":"string","format":"date-time","example":"2026-05-28T10:00:00.000Z","description":"ISO timestamp of the most recent execution"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"hrbp.TalentReview":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"TR-0001"},"number":{"type":"string","readOnly":true,"example":"TR-0000001"},"name":{"type":"string","example":"Q2 Regional Talent Review"},"state":{"type":"string","enum":["in_preparation","scheduled","in_progress","action_plan","closed"],"description":"Lifecycle (forward-only; SN label 'In Progress' → 'in_progress').","example":"in_progress"},"period_start":{"type":"string","format":"date","example":"2025-04-01"},"period_end":{"type":"string","format":"date","example":"2025-06-30"},"leaders":{"type":"array","description":"Employee IDs leading this review (SN leaders→sys_user).","items":{"type":"string"}},"departments":{"type":"array","description":"Department names in scope.","items":{"type":"string"}},"locations":{"type":"array","description":"Locations in scope.","items":{"type":"string"}},"template_id":{"type":"string","nullable":true},"objectives":{"type":"string","description":"Review objective(s) (e.g. 'Identify retention risks in high-growth hubs')."},"metrics":{"type":"array","description":"Metrics attached to the review.","items":{"type":"object","properties":{"metric_id":{"type":"string","example":"voluntary_attrition_rate"},"group_by":{"type":"string","nullable":true,"example":"department"},"aggregation":{"type":"string","nullable":true,"enum":["count","sum","avg"]},"order":{"type":"integer"},"insights":{"type":"string","nullable":true}}}},"active":{"type":"boolean","default":true},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}},"required":["name"]},"hrbp.EmployeeAssessment":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"ASSESS-0001"},"employee_id":{"type":"string","example":"EMP-0001"},"talent_review_id":{"type":"string","nullable":true},"assessment_date":{"type":"string","format":"date"},"cycle":{"type":"string","example":"CY2025"},"performance_rating":{"type":"integer","minimum":1,"maximum":5,"description":"1–5 (SN current_performance_level 10–50 / current_rating 30/40/50)."},"performance_band":{"type":"string","enum":["excellent","good","average","needs_improvement","poor"],"description":"Derived band (excellent/good/average/needs_improvement/poor)."},"potential":{"type":"string","enum":["low","medium","high"],"description":"SN 10/20/30 or High/Medium/Low."},"achievable_level":{"type":"string","enum":["1","2","3"],"description":"SN 10/20/30 or '1/2/3 Levels'."},"flight_risk":{"type":"string","enum":["low","medium","high"],"description":"Stored (not derived) per SN assessment."},"replacement_readiness":{"type":"string","enum":["immediate","3m","6m","12m"],"description":"SN 0/3/6/12."},"box":{"type":"integer","minimum":1,"maximum":9,"readOnly":true,"description":"Derived 9-box cell (potential × performance)."},"core_competencies":{"type":"string","nullable":true},"development_needs":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}},"required":["employee_id"]},"hrbp.PerformanceReview":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"PR-0001"},"employee_id":{"type":"string","example":"EMP-0001"},"review_start_date":{"type":"string","format":"date"},"review_end_date":{"type":"string","format":"date"},"rating_manager":{"type":"number","minimum":1,"maximum":5,"example":4.2},"rating_employee":{"type":"number","minimum":1,"maximum":5,"example":4},"assessed_on":{"type":"string","format":"date-time"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}},"required":["employee_id"]},"hrbp.SuccessionPlan":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"SP-0001"},"number":{"type":"string","readOnly":true,"example":"SP-0000001"},"title":{"type":"string","example":"Principal Product Manager"},"incumbent_employee_id":{"type":"string","nullable":true,"example":"EMP-0001"},"job_title":{"type":"string","nullable":true},"department":{"type":"string","nullable":true},"planned_retirement_date":{"type":"string","format":"date","nullable":true},"criticality":{"type":"string","enum":["low","medium","high"],"description":"SN demo low/medium/high."},"loss_impact":{"type":"string","enum":["low","medium","high"],"description":"SN High/Intermediate→medium/Low."},"retention_risk":{"type":"string","enum":["low","medium","high"]},"success_profile":{"type":"object","nullable":true,"description":"Role mission/skills/qualifications (SN JSON)."},"successor_count":{"type":"integer","readOnly":true},"external_id":{"type":"string","nullable":true},"active":{"type":"boolean","default":true},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}},"required":["title"]},"hrbp.Successor":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"SUC-0001"},"succession_plan_id":{"type":"string","example":"SP-0001"},"candidate_employee_id":{"type":"string","example":"EMP-0002"},"top_candidate":{"type":"boolean"},"readiness":{"type":"string","enum":["immediate","3m","6m","12m"],"description":"SN 0/3/6/12 or 'Ready in …' (9 months→6m)."},"potential":{"type":"string","enum":["low","medium","high"]},"achievable_level":{"type":"string","enum":["1","2","3"]},"current_rating":{"type":"string","enum":["excellent","good","average","needs_improvement","poor"],"description":"Banded (SN '4 - Exceeds Expectations')."},"education":{"type":"string","nullable":true},"skills":{"type":"string","nullable":true,"description":"Comma-separated."},"job_history_companies":{"type":"string","nullable":true},"promotion_readiness":{"type":"string","nullable":true},"development_areas":{"type":"string","nullable":true},"matching_score":{"type":"integer","minimum":0,"maximum":100,"nullable":true},"candidate_profile":{"type":"object","nullable":true,"description":"Per-dimension match scores (SN JSON)."},"temporary_fill":{"type":"boolean","nullable":true},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}},"required":["succession_plan_id","candidate_employee_id"]},"hrbp.Snapshot":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"SNP-0001"},"metric_id":{"type":"string","example":"headcount"},"group_id":{"type":"string","nullable":true,"description":"Batch grouping key."},"source":{"type":"string","enum":["local","workday","successfactors","oracle"],"default":"local"},"value":{"type":"object","description":"Computed metric value as JSON (per group)."},"timestamp":{"type":"string","format":"date-time"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}},"required":["metric_id","value"]},"hrbp.MetricGoal":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"GOAL-0001"},"name":{"type":"string","example":"Attrition Rate Goal 2026"},"talent_review_id":{"type":"string","nullable":true},"metric_id":{"type":"string","example":"attrition_rate"},"period_start":{"type":"string","format":"date"},"period_end":{"type":"string","format":"date"},"threshold_value":{"type":"number","example":3},"comparison_type":{"type":"string","enum":["gt","lt","eq","lte","gte"],"example":"lte"},"active":{"type":"boolean","default":true},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}},"required":["metric_id"]},"hrbp.HrbpScopeAssignment":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"SCOPE-0001"},"user":{"type":"string","description":"User (email/employee id) this scope grants visibility to.","example":"sofia.rossi@acme.com"},"scope_type":{"type":"string","enum":["hrbp","metric_threshold"],"default":"hrbp"},"scope_id":{"type":"string","nullable":true,"description":"Optional link to an HrbpScope (normalized scope definition).","example":"HSCOPE-RND"},"all_access":{"type":"boolean","default":false,"description":"Unrestricted (admin-equivalent) scope — request passes through verbatim."},"leaders":{"type":"array","description":"Leader employee IDs in scope.","items":{"type":"string"}},"departments":{"type":"array","description":"Departments in scope.","items":{"type":"string"}},"locations":{"type":"array","description":"Hierarchical geo scope: region / country / city tokens (a region token covers all its countries & cities).","items":{"type":"string"}},"active":{"type":"boolean","default":true},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}},"required":["user"]},"hrbp.HrbpScope":{"type":"object","description":"Named HRBP scope (SN sn_talent_pulse_hrbp_scope). Composed of HrbpScopeRule rows; assignments reference it by scope_id.","properties":{"id":{"type":"string","readOnly":true,"example":"HSCOPE-RND"},"name":{"type":"string","example":"R&D — AMS"},"description":{"type":"string","nullable":true},"active":{"type":"boolean","default":true},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}},"required":["name"]},"hrbp.HrbpScopeRule":{"type":"object","description":"A single dimension rule within an HrbpScope (SN sn_talent_pulse_hrbp_scope_rule).","properties":{"id":{"type":"string","readOnly":true,"example":"HRULE-0001"},"scope_id":{"type":"string","description":"Parent HrbpScope id.","example":"HSCOPE-RND"},"dimension":{"type":"string","enum":["leader","department","location","region","country","city","all_access"],"description":"Geo dimensions (location/region/country/city) all feed the hierarchical location scope.","example":"region"},"value":{"type":"string","nullable":true,"description":"The in-scope value for the dimension (null for all_access). Geo values may be a region (AMS), country (USA), or city (New York, NY).","example":"AMS"},"active":{"type":"boolean","default":true},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}},"required":["scope_id","dimension"]},"hrbp.SnapshotGroup":{"type":"object","description":"Groups related metric snapshots captured in one run (SN sn_talent_pulse_snapshot_group).","properties":{"id":{"type":"string","readOnly":true,"example":"SGRP-0001"},"name":{"type":"string","example":"March 2026 workforce snapshot"},"captured_at":{"type":"string","format":"date-time"},"source":{"type":"string","enum":["local","workday","successfactors","oracle"],"default":"local"},"note":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}},"required":["name"]},"hrbp.MetricSource":{"type":"object","description":"A system a metric value can originate from (SN sn_talent_pulse_metric_source).","properties":{"id":{"type":"string","readOnly":true,"example":"MSRC-local"},"key":{"type":"string","enum":["local","workday","successfactors","oracle"],"example":"local"},"name":{"type":"string","example":"Purple HRIS (local)"},"description":{"type":"string","nullable":true},"active":{"type":"boolean","default":true},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}},"required":["key","name"]},"hrbp.EmployeeRelation":{"type":"object","description":"Employee Relations case (SN sn_hr_er_case, extends sn_hr_core_case).","properties":{"id":{"type":"string","readOnly":true,"example":"ERC-0001"},"number":{"type":"string","readOnly":true,"example":"ERC-0000001"},"subject_person":{"type":"string","description":"Employee ID the case concerns.","example":"EMP-0003"},"short_description":{"type":"string","example":"Complaint of workplace harassment"},"description":{"type":"string","nullable":true},"state":{"type":"string","enum":["open","work_in_progress","awaiting_info","resolved","closed","cancelled"],"default":"open"},"priority":{"type":"string","enum":["low","moderate","high","critical"],"description":"SN 1-Critical/2-High/3-Moderate/4-Low.","default":"moderate"},"opened_by":{"type":"string","nullable":true,"description":"Employee ID of the HR rep who opened the case."},"assigned_to":{"type":"string","nullable":true,"description":"Employee ID of the assigned HR investigator."},"hr_service":{"type":"string","default":"Employee Relations"},"opened_at":{"type":"string","format":"date-time"},"due_date":{"type":"string","format":"date","nullable":true},"closed_at":{"type":"string","format":"date-time","nullable":true},"active":{"type":"boolean","default":true},"incident_date":{"type":"string","format":"date","nullable":true,"description":"Date of most recent incident."},"external_opened_for":{"type":"boolean","default":false,"description":"Opened for is not a User."},"locked":{"type":"boolean","default":false,"description":"Restricting access limits who can view/edit the case."},"preferred_contact_method":{"type":"string","enum":["not_specified","personal_phone","work_phone","personal_email","work_email"],"default":"not_specified"},"reported_date":{"type":"string","format":"date-time","nullable":true},"pip_start_date":{"type":"string","format":"date","nullable":true},"pip_end_date":{"type":"string","format":"date","nullable":true},"pip_improvement_areas":{"type":"string","nullable":true},"pip_rationale":{"type":"string","nullable":true},"pip_successful":{"type":"string","enum":["y","n"],"nullable":true},"pip_extended":{"type":"string","enum":["y","n"],"nullable":true},"pip_extension_date":{"type":"string","format":"date","nullable":true},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}},"required":["subject_person","short_description"]},"hrbp.AllegationType":{"type":"object","description":"Allegation type reference catalog (SN sn_hr_er_allegation_type).","properties":{"id":{"type":"string","readOnly":true,"example":"AT-HARASSMENT"},"name":{"type":"string","example":"Harassment"},"description":{"type":"string","nullable":true},"active":{"type":"boolean","default":true},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}},"required":["name"]},"hrbp.AllegationSubtype":{"type":"object","description":"Allegation subtype, dependent on allegation_type (SN sn_hr_er_allegation_subtype).","properties":{"id":{"type":"string","readOnly":true,"example":"AST-VERBAL"},"allegation_type_id":{"type":"string","example":"AT-HARASSMENT"},"name":{"type":"string","example":"Verbal harassment"},"active":{"type":"boolean","default":true},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}},"required":["allegation_type_id","name"]},"hrbp.Allegation":{"type":"object","description":"One allegation filed on an ER case (SN sn_hr_er_allegation).","properties":{"id":{"type":"string","readOnly":true,"example":"ALG-0001"},"employee_relation_id":{"type":"string","example":"ERC-0001"},"allegation_type_id":{"type":"string","example":"AT-HARASSMENT"},"allegation_subtype_id":{"type":"string","example":"AST-VERBAL"},"description":{"type":"string","nullable":true},"subjects_of_allegation":{"type":"string","readOnly":true,"nullable":true,"description":"Derived: names of the case's 'respondent' involved parties."},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}},"required":["employee_relation_id","allegation_type_id","allegation_subtype_id"]},"hrbp.InvolvedParty":{"type":"object","description":"A complainant, respondent, witness, or other party on an ER case (SN sn_hr_er_involved_party).","properties":{"id":{"type":"string","readOnly":true,"example":"IP-0001"},"employee_relation_id":{"type":"string","example":"ERC-0001"},"employee_id":{"type":"string","nullable":true,"description":"Employee ID, if the party has a User record.","example":"EMP-0008"},"not_in_system":{"type":"boolean","default":false,"description":"Involved party does not have a User record."},"name":{"type":"string","nullable":true,"description":"Name/description, used when not_in_system (or no employee_id)."},"display_name":{"type":"string","readOnly":true,"description":"Derived: the linked employee's name, else name."},"type":{"type":"string","enum":["complainant","respondent","witness","other"],"default":"other"},"correlation_id":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}},"required":["employee_relation_id","type"]},"hris.Employee":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"EMP-0001"},"firstName":{"type":"string","description":"Employee first name. Alias accepted on create/update: first_name","example":"Alice"},"lastName":{"type":"string","description":"Employee last name. Alias accepted on create/update: last_name","example":"Smith"},"email":{"type":"string","format":"email","example":"alice.smith0@acme.com"},"department":{"type":"string","description":"Department name. Must match an existing department. Alias accepted on create: department_id","example":"Engineering"},"title":{"type":"string","example":"Software Engineer"},"status":{"type":"string","enum":["active","terminated","on_leave"],"description":"Employment status. On PATCH also accepts 'suspended'. Use the /transition endpoint for lifecycle transitions.","example":"active"},"startDate":{"type":"string","format":"date","description":"Employment start date. Alias accepted on create: hire_date","example":"2024-01-15"},"salary":{"type":"number","example":120000},"managerId":{"type":"string","nullable":true,"description":"ID of the manager employee. Alias accepted on create: manager_id","example":"EMP-0003"},"phone":{"type":"string","example":"+1-415-555-1234"},"location":{"type":"string","description":"City (leaf of the location hierarchy city ∈ country ∈ region).","example":"San Francisco, CA"},"country":{"type":"string","nullable":true,"description":"Country (derived from city). Middle level of the location hierarchy; drives HRBP country-level scope.","example":"USA"},"timezone":{"type":"string","example":"America/Los_Angeles"},"costCenter":{"type":"string","example":"CC-ENG-100"},"region":{"type":"string","nullable":true,"enum":["AMS","EMEA","APAC","LATAM"],"description":"Geographic region (derived from location/city). Top level of the location hierarchy; used by HRBP analytics group-by + region scope.","example":"AMS"},"job_level":{"type":"string","nullable":true,"description":"Job level code (IC1–IC6 / M1–M7). Alias accepted on create: level.","example":"IC3"},"management_level":{"type":"string","nullable":true,"description":"Individual Contributor or Managerial.","example":"Individual Contributor"},"employment_type":{"type":"string","nullable":true,"enum":["full_time","contingent"],"description":"Worker classification. Drives contingent-headcount analytics.","example":"full_time"},"probation_end_date":{"type":"string","format":"date","nullable":true,"description":"End of probation period; null once past probation. Drives probation-headcount analytics.","example":"2025-08-15"},"termination_type":{"type":"string","nullable":true,"enum":["voluntary","involuntary"],"description":"Set when status→terminated via /transition.","example":"voluntary"},"termination_reason":{"type":"string","nullable":true,"description":"Categorized exit reason (e.g. compensation, career_growth, relocation, performance, retirement).","example":"career_growth"},"termination_date":{"type":"string","format":"date","nullable":true,"readOnly":true,"description":"Effective termination date (mirrors the latest 'terminated' status_history entry).","example":"2025-09-30"},"pto_balance":{"type":"object","nullable":true,"description":"Current PTO balance. Use PATCH /employees/{id}/pto-balance to update directly.","properties":{"vacation_days":{"type":"number","example":15},"sick_days":{"type":"number","example":5}}},"status_history":{"type":"array","readOnly":true,"description":"Immutable audit log of status transitions (appended by /transition endpoint).","items":{"type":"object","properties":{"status":{"type":"string"},"reason":{"type":"string","nullable":true},"effective_date":{"type":"string","format":"date-time"},"recorded_at":{"type":"string","format":"date-time"}}}},"skills":{"type":"array","description":"Employee skills with proficiency. Settable via PATCH /employees/{id}. Also available at GET /employees/{id}/skills.","items":{"type":"object","properties":{"skill":{"type":"string","example":"Python"},"proficiency":{"type":"string","enum":["beginner","intermediate","advanced","expert"],"example":"advanced"},"last_updated":{"type":"string","format":"date-time","example":"2026-01-15T00:00:00.000Z"}}}},"reviews":{"type":"array","description":"Recent performance reviews. Settable via PATCH /employees/{id}. Also available at GET /employees/{id}/reviews.","items":{"type":"object","properties":{"id":{"type":"string","example":"REV-0001-1"},"period":{"type":"string","example":"Annual 2025"},"rating":{"type":"string","enum":["needs_improvement","meets_expectations","exceeds_expectations"],"example":"meets_expectations"},"strengths":{"type":"array","items":{"type":"string"},"example":["Strong ownership and follow-through"]},"gaps":{"type":"array","items":{"type":"string"},"example":["Could delegate more"]},"reviewer_id":{"type":"string","nullable":true,"example":"EMP-0003"},"reviewed_at":{"type":"string","format":"date-time","example":"2026-02-01T00:00:00.000Z"}}}},"goals":{"type":"array","description":"Current development goals. Settable via PATCH /employees/{id}. Also available at GET /employees/{id}/goals.","items":{"type":"object","properties":{"id":{"type":"string","example":"GOAL-0001-1"},"description":{"type":"string","example":"Lead a cross-team initiative end to end"},"target_role":{"type":"string","nullable":true,"example":"Senior Engineer"},"target_timeline":{"type":"string","nullable":true,"example":"Q4 2026"},"status":{"type":"string","enum":["not_started","in_progress","completed"],"example":"in_progress"},"progress_pct":{"type":"integer","minimum":0,"maximum":100,"example":40}}}},"generate_related_records":{"type":"boolean","description":"CREATE only. When true (or camelCase generateRelatedRecords), seeds 3 timesheets and 2 benefits for the new employee. Not stored.","example":false},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}},"required":["firstName","lastName","email"]},"hris.Department":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"DEPT-001"},"name":{"type":"string","example":"Engineering"},"manager":{"type":"string","nullable":true,"description":"Email of the department manager. Aliases accepted on create: managerId, head_employee_id","example":"alice@acme.com"},"budget":{"type":"number","nullable":true,"example":500000},"headCount":{"type":"integer","description":"Number of employees in the department. Alias accepted on create: headcount (lowercase)","example":25},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}},"required":["name"]},"hris.PayRun":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"PR-001"},"periodStart":{"type":"string","format":"date","description":"Pay period start date. Alias accepted on create: period_start","example":"2026-05-01"},"periodEnd":{"type":"string","format":"date","description":"Pay period end date. Alias accepted on create: period_end","example":"2026-05-31"},"status":{"type":"string","enum":["draft","processing","completed","cancelled"],"description":"Pay run status. State machine: draft → processing → completed. Completed runs are locked. Cancelled runs cannot be reactivated.","example":"draft"},"totalAmount":{"type":"number","description":"Total gross payroll amount. Alias accepted on create: total_amount","example":350000},"employeeCount":{"type":"integer","description":"Number of employees included in this run. Alias accepted on create: employee_count","example":12},"runDate":{"type":"string","format":"date-time","nullable":true,"example":"2026-05-31T18:00:00.000Z"},"entries":{"type":"array","description":"Per-employee pay breakdown. Set on create or PATCH.","items":{"type":"object","properties":{"employeeId":{"type":"string","example":"EMP-0001"},"grossPay":{"type":"number","example":9000},"netPay":{"type":"number","example":6750}}}},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"hris.Timesheet":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"TS-0001"},"employee_id":{"type":"string","example":"EMP-0001"},"week_start":{"type":"string","format":"date","example":"2026-05-26"},"hours_worked":{"type":"number","minimum":0,"maximum":60,"example":40},"overtime_hours":{"type":"number","minimum":0,"maximum":20,"example":0},"status":{"type":"string","enum":["submitted","approved","rejected"],"example":"submitted"},"created_at":{"type":"string","format":"date-time","readOnly":true},"updated_at":{"type":"string","format":"date-time","readOnly":true}}},"hris.Benefit":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"BEN-001"},"employee_id":{"type":"string","example":"EMP-0001"},"type":{"type":"string","enum":["insurance","retirement","wellness","health"],"example":"insurance"},"plan_name":{"type":"string","example":"Medical - PPO"},"status":{"type":"string","enum":["active","pending","terminated"],"example":"active"},"enrollment_date":{"type":"string","format":"date","nullable":true,"example":"2026-01-01"},"created_at":{"type":"string","format":"date-time","readOnly":true},"updated_at":{"type":"string","format":"date-time","readOnly":true}}},"hris.ReviewCycle":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"RC-001"},"name":{"type":"string","example":"Annual 2024"},"period":{"type":"string","example":"Annual 2024"},"status":{"type":"string","enum":["planning","in_progress","completed","cancelled"],"example":"planning"},"employee_id":{"type":"string","nullable":true,"example":"EMP-0001"},"manager_id":{"type":"string","nullable":true,"example":"EMP-0003"},"rating":{"type":"string","enum":["meets_expectations","exceeds_expectations","needs_improvement"],"nullable":true,"example":"meets_expectations"},"notes":{"type":"string","example":""},"created_at":{"type":"string","format":"date-time","readOnly":true},"updated_at":{"type":"string","format":"date-time","readOnly":true}}},"hris.TimeOffRequest":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"TOR-0001"},"employee_id":{"type":"string","description":"ID of the requesting employee. Alias accepted on create: employeeId","example":"EMP-0001"},"type":{"type":"string","enum":["vacation","sick","personal","bereavement","jury_duty"],"description":"Kind of time off. Defaults to 'vacation'.","example":"vacation"},"start_date":{"type":"string","format":"date","description":"First day off (inclusive). Alias accepted on create: startDate","example":"2026-07-01"},"end_date":{"type":"string","format":"date","description":"Last day off (inclusive). Alias accepted on create: endDate","example":"2026-07-05"},"hours":{"type":"number","nullable":true,"description":"Total hours requested","example":32},"note":{"type":"string","nullable":true,"description":"Optional reason / note for the request","example":"Family vacation"},"status":{"type":"string","enum":["pending","approved","denied","cancelled"],"description":"Request status. Defaults to 'pending' on create; approve/deny via PATCH.","example":"pending"},"created_at":{"type":"string","format":"date-time","readOnly":true},"updated_at":{"type":"string","format":"date-time","readOnly":true}},"required":["employee_id","start_date","end_date"]},"hris.WorkAuthorization":{"type":"object","description":"An employee's right-to-work record for a country: visa/permit type, status, expiry, and renewal tracking. Citizens and permanent residents have no expiry. The agent stitches this with Purple Expense trips to answer travel-eligibility questions.","properties":{"id":{"type":"string","readOnly":true,"example":"WA-0001"},"employee_email":{"type":"string","format":"email","example":"alice.chen@acme.com","description":"The employee this authorization belongs to (matches an Employee.email and the canonical Identity directory)."},"country":{"type":"string","enum":["IN","AU","US","GB","KR","DE"],"example":"US","description":"Country this authorization grants the right to work in."},"authorization_type":{"type":"string","enum":["citizen","permanent_resident","h1b","l1","tn","opt","subclass_482","subclass_186","employment_visa","intra_company_transfer","skilled_worker","global_talent","senior_specialist_worker","e7_skilled_worker","d8_corporate_investment","f2_resident","eu_blue_card","ict_permit","skilled_immigration_visa"],"example":"h1b"},"status":{"type":"string","enum":["valid","expiring_soon","expired","renewal_pending","renewal_filed"],"example":"valid"},"issued_date":{"type":"string","format":"date","nullable":true,"example":"2023-01-15"},"expiry_date":{"type":"string","format":"date","nullable":true,"example":"2026-09-30","description":"Null for citizen / permanent_resident."},"sponsoring_entity":{"type":"string","nullable":true,"example":"Acme Corp","description":"Employer/entity sponsoring the visa, if any."},"renewal_status":{"type":"string","enum":["not_required","not_started","renewal_pending","renewal_filed"],"example":"not_required"},"renewal_due_date":{"type":"string","format":"date","nullable":true,"example":"2026-06-30"},"travel_restrictions":{"type":"array","items":{"type":"string"},"example":["no_intl_travel_while_renewal_pending"],"description":"Advisories that affect international travel (e.g. advance_parole_required)."},"notes":{"type":"string","example":""},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"hris.JobFamily":{"type":"object","description":"Career family grouping related job profiles (reference catalog).","properties":{"id":{"type":"string","readOnly":true,"example":"JF-ENG"},"name":{"type":"string","example":"Engineering"},"description":{"type":"string","example":"Software engineering and infrastructure."},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}},"required":["name"]},"hris.JobLevel":{"type":"object","description":"Job level / grade in the leveling framework (reference catalog).","properties":{"id":{"type":"string","readOnly":true,"example":"IC3","description":"Level code (IC1–IC6 / M1–M5)."},"name":{"type":"string","example":"Individual Contributor 3"},"track":{"type":"string","enum":["IC","M"],"description":"Individual-contributor or management track.","example":"IC"},"tier":{"type":"integer","description":"Numeric rank within the track.","example":3},"management_level":{"type":"string","enum":["Individual Contributor","Managerial"],"example":"Individual Contributor"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}},"required":["name"]},"hris.JobProfile":{"type":"object","description":"Job profile (role template) tying a title to a family + level (reference catalog).","properties":{"id":{"type":"string","readOnly":true,"example":"JP-SWE"},"title":{"type":"string","example":"Software Engineer"},"job_family":{"type":"string","description":"JobFamily name.","example":"Engineering"},"job_level":{"type":"string","description":"JobLevel code.","example":"IC3"},"management_level":{"type":"string","enum":["Individual Contributor","Managerial"],"example":"Individual Contributor"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}},"required":["title"]},"hris.JobRole":{"type":"object","description":"Job role under a family, optionally mapped to a job profile (reference catalog; modeled on sn_hr_core_position).","properties":{"id":{"type":"string","readOnly":true,"example":"JR-SWE"},"name":{"type":"string","example":"Software Engineer"},"description":{"type":"string","example":"Builds and maintains platform services."},"active":{"type":"boolean","example":true},"job_family":{"type":"string","description":"JobFamily name.","example":"Engineering"},"job_profile":{"type":"string","description":"JobProfile id.","example":"JP-SWE"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}},"required":["name"]},"identity.Person":{"type":"object","description":"A person in the canonical directory.","properties":{"id":{"type":"string","readOnly":true,"example":"PERSON-0001"},"email":{"type":"string","format":"email","example":"alice.chen@acme.com"},"firstName":{"type":"string","example":"Alice"},"lastName":{"type":"string","example":"Chen"},"fullName":{"type":"string","example":"Alice Chen"},"title":{"type":"string","example":"Engineering Manager"},"department":{"type":"string","example":"Engineering"},"managerEmail":{"type":"string","nullable":true,"example":"frank.kim@acme.com"},"country":{"type":"string","enum":["IN","AU","US","GB","KR","DE"],"example":"US"},"locale":{"type":"string","example":"en-US"},"city":{"type":"string","example":"San Francisco"},"region":{"type":"string","example":"CA"},"location":{"type":"string","example":"San Francisco, CA"},"timezone":{"type":"string","example":"America/Los_Angeles"},"phone":{"type":"string","example":"+1 (415) 555-1234"},"avatarColor":{"type":"string","example":"#4285F4"},"startDate":{"type":"string","format":"date","example":"2023-04-12"},"workStatus":{"type":"string","enum":["FULL_TIME","CONTINGENT","INTERN"],"example":"FULL_TIME"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"identity.IdentityGatewayUser":{"type":"object","description":"A single user record in the Moveworks Identity Gateway shape (projection of a Person).","properties":{"user":{"type":"object","properties":{"state":{"type":"string","enum":["ACTIVE","INACTIVE"]},"universal_identifier":{"type":"string"},"email_addr":{"type":"string","format":"email"},"first_name":{"type":"string"},"last_name":{"type":"string"},"full_name":{"type":"string"},"work_status":{"type":"string","enum":["UNKNOWN_WORK_STATUS","CONTINGENT","INTERN","FULL_TIME"]},"employment_info":{"type":"object","properties":{"employee_start_date_ts":{"type":"string"},"role":{"type":"string"},"manager_email":{"type":"string"},"cost_center_id":{"type":"string"},"cost_center_name":{"type":"string"},"department":{"type":"string"},"office_phone_number":{"type":"string"},"assistant_full_name":{"type":"string"},"employment_location":{"type":"object","properties":{"location":{"type":"string"},"office":{"type":"string"},"country_code":{"type":"string"},"region":{"type":"string"},"timezone":{"type":"string"}}}}}}},"system_identity":{"type":"object","properties":{"id":{"type":"string"},"username":{"type":"string"}}},"last_updated_at":{"type":"string","format":"date-time"}}},"itsm.Incident":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"INC-0001"},"title":{"type":"string","example":"VPN issue - Engineering"},"status":{"type":"string","enum":["open","in_progress","resolved","closed","cancelled"],"example":"open"},"priority":{"type":"string","enum":["P1","P2","P3","P4"],"example":"P2"},"category":{"type":"string","enum":["hardware","software","network","access","other"],"example":"network"},"assignee":{"type":"string","format":"email","example":"alice@acme.com"},"reporter":{"type":"string","format":"email","example":"user1@acme.com"},"escalation_tier":{"type":"integer","minimum":1,"maximum":3,"example":1},"escalated_at":{"type":"string","format":"date-time","readOnly":true},"escalation_notes":{"type":"string","example":"Escalated due to customer impact"},"reopen_reason":{"type":"string","example":"Issue recurred after resolution"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"itsm.Change":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"CHG-0001"},"title":{"type":"string","example":"OS upgrade"},"status":{"type":"string","enum":["draft","pending_approval","approved","in_progress","implemented","completed","rejected","cancelled"],"example":"draft"},"riskLevel":{"type":"string","enum":["low","medium","high"],"example":"medium"},"scheduledDate":{"type":"string","format":"date-time","example":"2026-06-15T00:00:00.000Z"},"approver":{"type":"string","format":"email","example":"manager1@acme.com"},"cab_approval":{"type":"object","readOnly":true,"properties":{"approved_by":{"type":"string"},"approved_at":{"type":"string","format":"date-time"},"rejected_by":{"type":"string"},"reason":{"type":"string"}}},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"itsm.SlaPolicy":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"SLA-001"},"name":{"type":"string","example":"Critical Incident SLA"},"responseTimeHours":{"type":"number","example":1},"resolutionTimeHours":{"type":"number","example":4},"priority":{"type":"string","enum":["P1","P2","P3","P4"],"example":"P1"},"active":{"type":"boolean","example":true},"paused":{"type":"boolean","example":false},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"itsm.Problem":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"PRB-0001"},"title":{"type":"string","example":"Recurring login failures"},"status":{"type":"string","enum":["open","known_error","resolved"],"example":"open"},"rootCause":{"type":"string","enum":["Unknown","Configuration error","Software bug","Infrastructure issue","Third-party dependency"],"example":"Unknown"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"itsm.ServiceRequest":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"SR-0001"},"title":{"type":"string","example":"New laptop request"},"status":{"type":"string","enum":["submitted","approved","in_progress","completed","cancelled"],"example":"submitted"},"requestedBy":{"type":"string","format":"email","example":"user1@acme.com"},"fulfillmentGroup":{"type":"string","enum":["IT Support","Security","Procurement","Facilities"],"example":"IT Support"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"itsm.ConfigurationItem":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"CI-a1b2c3d4"},"name":{"type":"string","example":"web-server-01"},"type":{"type":"string","enum":["server","workstation","network_device","application","database","storage"],"example":"server"},"status":{"type":"string","enum":["operational","maintenance","retired","ordered"],"example":"operational"},"environment":{"type":"string","enum":["production","staging","development","dr"],"example":"production"},"owner":{"type":"string","enum":["IT Ops","Engineering","Security"],"example":"IT Ops"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"itsm.KbArticle":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"KB-0001"},"title":{"type":"string","example":"How to reset your password"},"category":{"type":"string","enum":["Authentication","Network","Hardware","Software","Security","Onboarding"],"example":"Authentication"},"views":{"type":"integer","example":150},"helpful":{"type":"integer","example":42},"author":{"type":"string","format":"email","example":"alice@acme.com"},"publishedAt":{"type":"string","format":"date-time","example":"2026-01-10T00:00:00.000Z"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"itsm.Alert":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"ALT-0001"},"title":{"type":"string","example":"High CPU usage detected"},"severity":{"type":"string","enum":["critical","warning","info"],"example":"warning"},"source":{"type":"string","enum":["Nagios","Datadog","PagerDuty","CloudWatch","Prometheus"],"example":"Datadog"},"status":{"type":"string","enum":["active","acknowledged","resolved"],"example":"active"},"triggeredAt":{"type":"string","format":"date-time","example":"2026-05-29T08:00:00.000Z"},"resolvedAt":{"type":"string","format":"date-time","nullable":true,"example":null},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"lms.Course":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"CRS-0001"},"title":{"type":"string","example":"Foundations of People Management"},"description":{"type":"string","example":"A practical course covering the fundamentals of leading a team."},"skills_taught":{"type":"array","description":"Skills this course develops (matches Purple HRS employee skills).","items":{"type":"string"},"example":["People Management","Coaching","Communication"]},"level":{"type":"string","enum":["beginner","intermediate","advanced"],"example":"beginner"},"duration_minutes":{"type":"integer","example":90},"format":{"type":"string","enum":["video","article","path"],"example":"video"},"provider":{"type":"string","example":"Purple Academy"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}},"required":["title"]},"lms.Enrollment":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"ENR-0001"},"course_id":{"type":"string","example":"CRS-0001"},"learner":{"type":"string","description":"Resolved learner identifier (employee_id or user_email). Set automatically on enroll.","example":"EMP-0001"},"employee_id":{"type":"string","nullable":true,"example":"EMP-0001"},"user_email":{"type":"string","nullable":true,"format":"email","example":"alice.smith0@acme.com"},"status":{"type":"string","enum":["not_started","in_progress","completed","dropped"],"example":"in_progress"},"progress_pct":{"type":"integer","minimum":0,"maximum":100,"example":40},"score":{"type":"number","nullable":true,"minimum":0,"maximum":100,"example":88},"enrolled_at":{"type":"string","format":"date-time","example":"2026-05-01T00:00:00.000Z"},"completed_at":{"type":"string","format":"date-time","nullable":true,"example":"2026-05-20T00:00:00.000Z"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"manager-experience.TeamMembership":{"type":"object","description":"MEA-local team overlay. When present, the ServiceNow/Workday/SFSF directReports+reportees endpoints derive the manager→reports relationship from these rows instead of the shared roster's roster-wide `managerEmail` — so this app can present 2 realistic-sized teams even though the shared roster's random assignments would give sparser results. Not read by other Purple Suite apps.","properties":{"id":{"type":"string","readOnly":true,"example":"TEAM-A"},"team_name":{"type":"string","example":"Engineering"},"manager_email":{"type":"string","format":"email"},"member_emails":{"type":"array","items":{"type":"string","format":"email"}},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"manager-experience.Chat":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"CHAT-0001"},"chatType":{"type":"string","enum":["oneOnOne","group"],"example":"oneOnOne"},"topic":{"type":"string","nullable":true,"example":"Q3 roadmap sync"},"member_emails":{"type":"array","items":{"type":"string","format":"email"},"example":["alice.smith0@acme.com","bob.jones1@acme.com"]},"createdDateTime":{"type":"string","format":"date-time"},"lastUpdatedDateTime":{"type":"string","format":"date-time"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"manager-experience.ChatMessage":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"CMSG-0001"},"chat_id":{"type":"string","example":"CHAT-0001"},"from_email":{"type":"string","format":"email"},"from_name":{"type":"string"},"body_content":{"type":"string"},"body_content_type":{"type":"string","enum":["text","html"],"example":"text"},"createdDateTime":{"type":"string","format":"date-time"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"manager-experience.Team":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"TEAM-0001"},"displayName":{"type":"string","example":"Engineering"},"description":{"type":"string","nullable":true},"member_emails":{"type":"array","items":{"type":"string","format":"email"}},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"manager-experience.Channel":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"CHAN-0001"},"team_id":{"type":"string","example":"TEAM-0001"},"displayName":{"type":"string","example":"General"},"description":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"manager-experience.ChannelMessage":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"CHMSG-0001"},"team_id":{"type":"string"},"channel_id":{"type":"string"},"from_email":{"type":"string","format":"email"},"from_name":{"type":"string"},"body_content":{"type":"string"},"body_content_type":{"type":"string","enum":["text","html"]},"createdDateTime":{"type":"string","format":"date-time"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"manager-experience.MailMessage":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"MAIL-0001"},"owner_email":{"type":"string","format":"email","description":"Mailbox owner (the caller of GET /v1.0/me/messages)."},"from_email":{"type":"string","format":"email"},"from_name":{"type":"string"},"to_emails":{"type":"array","items":{"type":"string","format":"email"}},"subject":{"type":"string"},"body_content":{"type":"string"},"body_content_type":{"type":"string","enum":["text","html"]},"receivedDateTime":{"type":"string","format":"date-time"},"isRead":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"manager-experience.JiraProject":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"JPROJ-0001"},"key":{"type":"string","example":"ENG"},"name":{"type":"string","example":"Engineering"},"lead_email":{"type":"string","format":"email"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"manager-experience.JiraSprint":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"SPR-0001"},"name":{"type":"string","example":"Sprint 24"},"state":{"type":"string","enum":["active","closed","future"]},"project_key":{"type":"string","example":"ENG"},"startDate":{"type":"string","format":"date"},"endDate":{"type":"string","format":"date"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"manager-experience.JiraIssue":{"type":"object","description":"Data-layer storage shape. The /rest/api/3/search/jql contract response re-shapes this into real Jira issue JSON (fields.customfield_10020 for sprint, fields.assignee without emailAddress, etc.) — see contract.ts.","properties":{"id":{"type":"string","readOnly":true,"example":"JISS-0001"},"key":{"type":"string","example":"ENG-142"},"project_key":{"type":"string","example":"ENG"},"project_name":{"type":"string","example":"Engineering"},"sprint_id":{"type":"string","nullable":true,"example":"SPR-0001"},"summary":{"type":"string"},"status":{"type":"string","enum":["To Do","In Progress","Done"]},"assignee":{"type":"string","format":"email","nullable":true,"description":"Internal only — the real Jira response omits assignee email (privacy masking)."},"assignee_display_name":{"type":"string","nullable":true},"issuetype":{"type":"string","enum":["Story","Bug","Task"]},"priority":{"type":"string","enum":["Highest","High","Medium","Low"]},"duedate":{"type":"string","format":"date","nullable":true},"labels":{"type":"array","items":{"type":"string"},"example":["backend"]},"updated":{"type":"string","format":"date-time"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"manager-experience.Story":{"type":"object","description":"Data-layer storage shape for a ServiceNow Agile Development (rm_story) record. The /api/now/table/rm_story contract response re-shapes this into the real captured field set — reference fields (assigned_to, assignment_group) as {display_value, link}, and sprint fields dot-walked as flat literal keys (sprint.start_date, sprint.committed_points, …), matching this app's existing sys_user org-chain convention — see contract.ts.","properties":{"id":{"type":"string","readOnly":true,"example":"STORY-0001"},"number":{"type":"string","example":"STRY0010021"},"short_description":{"type":"string"},"description":{"type":"string"},"acceptance_criteria":{"type":"string"},"assigned_to":{"type":"string","format":"email","description":"Internal only — the contract response resolves this to the real Table API {display_value, link} reference shape."},"assignment_group":{"type":"string","example":"Engineering"},"state_id":{"type":"string","enum":["1","2","3","4"],"description":"Raw rm_story choice-list value the real sysparm_query filters on (1=Draft, 2=Work in Progress, 3=Complete, 4=Cancelled)."},"state":{"type":"string","example":"Work in Progress"},"blocked":{"type":"boolean","example":false,"description":"Whether the story is currently blocked (e.g. waiting on a dependency). stories-only, not on Defect."},"blocked_reason":{"type":"string","nullable":true,"example":null,"description":"Why the story is blocked. null when blocked is false. stories-only, not on Defect."},"priority":{"type":"string","example":"2 - High"},"story_points":{"type":"number","example":8},"end_date":{"type":"string","format":"date"},"project":{"type":"string","nullable":true,"description":"Internal only — the SPM (pm_project) project this story rolls up to. The contract response resolves this to a {display_value, link} reference, matching assigned_to/assignment_group's convention."},"epic":{"type":"string","nullable":true},"sprint_name":{"type":"string","nullable":true},"sprint_start_date":{"type":"string","format":"date","nullable":true},"sprint_end_date":{"type":"string","format":"date","nullable":true},"sprint_committed_points":{"type":"number","nullable":true},"sprint_story_points":{"type":"number","nullable":true},"sprint_actual_points":{"type":"number","nullable":true},"sys_updated_on":{"type":"string","format":"date-time"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"manager-experience.Defect":{"type":"object","description":"Data-layer storage shape for a ServiceNow Agile Development (rm_defect) record. The /api/now/table/rm_defect contract response re-shapes this the same way as Story — see contract.ts.","properties":{"id":{"type":"string","readOnly":true,"example":"DEFECT-0001"},"number":{"type":"string","example":"DFCT0010041"},"short_description":{"type":"string"},"description":{"type":"string"},"assigned_to":{"type":"string","format":"email","description":"Internal only — see Story.assigned_to."},"assignment_group":{"type":"string","example":"Engineering"},"state_id":{"type":"string","enum":["1","2","3","4","7"],"description":"Raw rm_defect choice-list value (1=Draft, 2=Work in Progress, 3=Complete, 4=Cancelled, 7=Deferred — defects carry this extra terminal state stories don't)."},"state":{"type":"string","example":"Work in Progress"},"priority":{"type":"string","example":"1 - Critical"},"end_date":{"type":"string","format":"date"},"sprint_name":{"type":"string","nullable":true},"sprint_start_date":{"type":"string","format":"date","nullable":true},"sprint_end_date":{"type":"string","format":"date","nullable":true},"sprint_committed_points":{"type":"number","nullable":true},"sys_updated_on":{"type":"string","format":"date-time"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"manager-experience.ResourceAllocation":{"type":"object","description":"Data-layer storage shape for one ServiceNow SPM resource_aggregate_weekly stats bucket (Servicenow_Get_Team_Project_Allocations, plugin Team_Project_Allocations). Every worker has an Availability and a Capacity row (no task); an allocated worker additionally has one Project/Demand Allocated row per active allocation. The /api/now/stats/resource_aggregate_weekly contract response re-shapes this into ServiceNow's real Stats API {stats, groupby_fields} envelope — see contract.ts.","properties":{"id":{"type":"string","readOnly":true,"example":"RALLOC-0001"},"worker_email":{"type":"string","format":"email","description":"Internal only — the contract response resolves this to the groupby 'user' field's display name (full name, not email)."},"category":{"type":"string","enum":["Availability","Capacity","Project Allocated","Demand Allocated"]},"task_number":{"type":"string","nullable":true,"example":"PRJ0010012","description":"The project/demand number. null for Availability/Capacity rows."},"task_short_description":{"type":"string","nullable":true},"task_state":{"type":"string","nullable":true,"example":"Work in Progress"},"task_sys_class_name":{"type":"string","nullable":true,"enum":["Project","Demand"]},"task_percent_complete":{"type":"number","nullable":true,"example":25},"sum_hours":{"type":"number","example":120},"sum_man_days":{"type":"number","example":15},"avg_fte":{"type":"number","example":0.057692},"avg_utilization":{"type":"number","nullable":true,"description":"Only meaningful on the Availability row; null on every other category."},"week_starts_on_min":{"type":"string","format":"date"},"week_starts_on_max":{"type":"string","format":"date"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"manager-experience.WorkdayLearningEnrollment":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"WLE-0001"},"worker_email":{"type":"string","format":"email"},"course":{"type":"string","example":"Foundations of People Management"},"registrationStatus":{"type":"string","enum":["Registered","Waitlisted","Cancelled"]},"completionStatus":{"type":"string","enum":["Not Started","In Progress","Complete"]},"dueDate1":{"type":"string","format":"date"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"manager-experience.WorkdayCertification":{"type":"object","description":"Data-layer storage shape. The WQL contract response re-shapes `certification`/`certification_id` into Workday's real {id,descriptor} reference object — see contract.ts. The real capture has no certificationNumber field.","properties":{"id":{"type":"string","readOnly":true,"example":"WCERT-0001"},"worker_email":{"type":"string","format":"email"},"certification":{"type":"string","example":"PMP","description":"Short code — becomes the reference object's `descriptor`."},"certification_id":{"type":"string","example":"CERT-PMP","description":"Becomes the reference object's `id`."},"certificationName":{"type":"string","example":"Project Management Professional"},"certifierOrIssuer":{"type":"string","example":"PMI"},"expirationDate":{"type":"string","format":"date","nullable":true},"examDate":{"type":"string","format":"date","nullable":true},"issuedDate":{"type":"string","format":"date"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"manager-experience.WorkdayLoa":{"type":"object","description":"Data-layer storage shape for a Workday leave of absence. The Absence Management REST contract response (contract.ts) wraps `worker`/`status_id`+`status`/`leaveType_id`+`leaveType` into Workday's real {id,descriptor} reference objects.","properties":{"id":{"type":"string","readOnly":true,"example":"WLOA-0001"},"worker_email":{"type":"string","format":"email"},"leaveType_id":{"type":"string","example":"MEDICAL_LOA","description":"Becomes the leaveType reference object's `id`."},"leaveType":{"type":"string","example":"Medical Leave of Absence","description":"Becomes the leaveType reference object's `descriptor`."},"status_id":{"type":"string","example":"e2d08afc53614c37b32b31270bb8bee3","description":"Becomes the status reference object's `id`; the In-Progress id the real action filters on."},"status":{"type":"string","example":"In Progress","description":"Becomes the status reference object's `descriptor`."},"businessProcessStepStatus":{"type":"string","example":"In Progress"},"firstDayOfLeave":{"type":"string","format":"date","example":"2026-06-01"},"lastDayOfWork":{"type":"string","format":"date","example":"2026-05-31"},"estimatedLastDayOfLeave":{"type":"string","format":"date","example":"2026-08-30"},"latestLeaveComment":{"type":"string","example":"Medical leave approved; return date is an estimate pending clearance."},"leaveImpactFields":{"type":"object","additionalProperties":{"type":"boolean"},"description":"Flat boolean map (schedulingEffect, absenceAccrualEffect, benefitEffect, payrollEffect, …)."},"additionalFields":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"descriptor":{"type":"string"},"name":{"type":"string"}}}},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"manager-experience.WorkdayTimeOff":{"type":"object","description":"Data-layer storage shape for a Workday time-off (PTO) entry. The Absence Management REST contract response (contract.ts) wraps `worker`/`status_id`+`status`/`timeOffType_id`+`timeOffType` into Workday's real {id,descriptor} reference objects.","properties":{"id":{"type":"string","readOnly":true,"example":"PTO-0001"},"worker_email":{"type":"string","format":"email"},"date":{"type":"string","format":"date","example":"2026-08-10"},"quantity":{"type":"number","example":8},"unitOfTime":{"type":"string","example":"HOURS"},"timeOffType_id":{"type":"string","example":"VACATION","description":"Becomes the timeOffType reference object's `id`."},"timeOffType":{"type":"string","example":"Vacation","description":"Becomes the timeOffType reference object's `descriptor`."},"status_id":{"type":"string","example":"APPROVED","description":"Becomes the status reference object's `id`."},"status":{"type":"string","example":"Approved","description":"Becomes the status reference object's `descriptor`; either 'Approved' or 'Submitted'."},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"manager-experience.SfsfCertification":{"type":"object","description":"Data-layer storage shape, field names matching the real Background_Certificates OData entity (licenseName/institution/licenseNumber — not certification/issuingOrg/certificationNumber).","properties":{"id":{"type":"string","readOnly":true,"example":"SFCERT-0001"},"worker_email":{"type":"string","format":"email"},"backgroundElementId":{"type":"integer","example":90000},"name":{"type":"string","example":"Certified ScrumMaster"},"description":{"type":"string"},"licenseName":{"type":"string","example":"Certified ScrumMaster"},"institution":{"type":"string","example":"Scrum Alliance"},"licenseNumber":{"type":"string","example":"CSM-5000"},"licenseCountry":{"type":"string","example":"USA"},"licenseState":{"type":"string"},"bgOrderPos":{"type":"string","example":"1"},"lastModifiedDate":{"type":"string","format":"date"},"startDate":{"type":"string","format":"date"},"endDate":{"type":"string","format":"date","nullable":true},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"manager-experience.HrProfileOverride":{"type":"object","description":"MEA-local overlay for the sn_hr_core_profile contract, seeded with one row per roster member (default date_of_birth = employment_start_date - 28y, employment_start_date = the roster's startDate) when the instance is created. Edit a row's date_of_birth/employment_start_date (e.g. via export → edit → import) to pin a specific birthday/anniversary for an eval — sn_hr_core_profile returns whatever is in this collection, falling back to the live-computed default only if a worker_email has no row at all (e.g. an instance created before this collection existed).","properties":{"id":{"type":"string","readOnly":true,"example":"HRP-alice.smith0"},"worker_email":{"type":"string","format":"email"},"date_of_birth":{"type":"string","format":"date","nullable":true},"employment_start_date":{"type":"string","format":"date","nullable":true},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"manager-experience.JourneyTask":{"type":"object","description":"Data-layer storage shape for one employee_tasks/manager_tasks row. The sn_jny contract response re-shapes this into ServiceNow's real task object ({task_type, ja_stage, journey_title, due_date_display_value, ...}) — see contract.ts.","properties":{"id":{"type":"string","readOnly":true,"example":"JNY-0001-T001"},"task_type":{"type":"string","enum":["Task","Approval"]},"ja_stage":{"type":"string","example":"Week 1"},"short_description":{"type":"string"},"due_date":{"type":"string","format":"date"},"is_over_due":{"type":"boolean"},"is_optional":{"type":"boolean"},"order":{"type":"string","example":"10"}}},"manager-experience.DiscussionRecord":{"type":"object","description":"Data-layer storage shape for one ServiceNow EGD Conversation Discussion (sn_egd_act_discussion) row — a manager-employee 1:1 discussion record that a saved meeting brief gets written onto. The sn_egd_act_discussion contract response re-shapes this into ServiceNow's real field set (discussion_series as a {value,link} reference, active as a string, etc.) — see contract.ts. Not every employee has one (sparse, like Journey) — that's what drives Update_1_1_Brief_on_Conversation_Record's 'no active discussion record found' branch.","properties":{"id":{"type":"string","readOnly":true,"example":"DISC-0001"},"employee_email":{"type":"string","format":"email"},"manager_email":{"type":"string","format":"email"},"u_agenda":{"type":"string","description":"The saved meeting brief/agenda text.","example":"Discussed Q3 goals and career growth plan."},"active":{"type":"boolean","example":true},"title":{"type":"string","example":"1:1 with Alex Smith"},"summary":{"type":"string"},"additional_details":{"type":"string"},"state":{"type":"string","example":"In Progress"},"calendar_provider":{"type":"string","nullable":true},"calendar_event_table":{"type":"string","nullable":true},"date":{"type":"string","format":"date"},"start_time":{"type":"string","format":"date-time"},"end_time":{"type":"string","format":"date-time"},"sys_mod_count":{"type":"integer","example":0},"sys_created_by":{"type":"string","example":"admin"},"sys_created_on":{"type":"string","format":"date-time"},"sys_updated_by":{"type":"string","example":"admin"},"sys_updated_on":{"type":"string","format":"date-time"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"manager-experience.TalkingPoint":{"type":"object","description":"Data-layer storage shape for one ServiceNow EGD Act Talking Point (sn_egd_act_talking_point) row — a manager's running agenda item to raise with a direct report in an upcoming 1:1, backing Manage_Discussion_Topics_for_an_Employee (talking_points_handler). added_by/u_added_for are Employee Profile sys_ids (sn_employee_profile), NOT sys_user sys_ids — see contract.ts's employeeProfileSysIdFor. Soft-deleted via active=false, never removed, so list_talking_points' active=true filter is the only thing that hides it.","properties":{"id":{"type":"string","readOnly":true,"example":"TP-0001"},"title":{"type":"string","example":"Discuss sprint velocity dip in Sprint 24"},"added_by":{"type":"string","description":"The manager's Employee Profile sys_id."},"u_added_for":{"type":"string","description":"The reportee's Employee Profile sys_id."},"active":{"type":"boolean","example":true},"sys_created_on":{"type":"string","format":"date-time"},"sys_updated_on":{"type":"string","format":"date-time"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"manager-experience.Journey":{"type":"object","description":"Data-layer storage shape for a ServiceNow Unified Journey (sn_jny) record — an employee's onboarding/career journey plan, backing Fetch_Journey_Details. The contract response re-shapes every scalar into ServiceNow's {value,displayValue} pair convention and nests employee_tasks/manager_tasks as managerTasks/employeeTasks — see contract.ts.","properties":{"id":{"type":"string","readOnly":true,"example":"JNY-0001"},"employee_email":{"type":"string","format":"email"},"manager_email":{"type":"string","format":"email"},"mentor_name":{"type":"string","nullable":true},"title":{"type":"string","example":"Alex's Engineering Onboarding"},"type":{"type":"string","enum":["Onboarding","Career Development"]},"status":{"type":"string","example":"In Progress"},"state":{"type":"string","enum":["active","closed"]},"config_type":{"type":"string","enum":["onboarding_plan","career_plan"]},"journey_config_name":{"type":"string","example":"Standard Engineering Onboarding Plan"},"created_on":{"type":"string","format":"date-time"},"published_on":{"type":"string","format":"date-time"},"sys_updated_on":{"type":"string","format":"date-time"},"progress_percentage":{"type":"integer","example":40},"employee_tasks":{"type":"array","items":{"$ref":"#/components/schemas/manager-experience.JourneyTask"}},"manager_tasks":{"type":"array","items":{"$ref":"#/components/schemas/manager-experience.JourneyTask"}},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"manager-experience.MicrosoftOoO":{"type":"object","description":"Per-worker out-of-office state shared by Reportees_MicrosoftGraph_OOO_Status and Employee_OOO_Status — both plugins call the identical real MS Graph routes (presence, mailboxSettings/automaticRepliesSetting, calendarView) for the same users, so one collection backs all of them (resolveOooOverride, contract.ts). One row per worker_email; scheduled_start/scheduled_end back presence and automaticRepliesSetting. calendarView uses calendar_start/calendar_end when set, otherwise falls back to scheduled_start/scheduled_end — set them separately when a person's calendar OOO block and mailbox auto-reply window genuinely end at different times. Event subject (and presence's outOfOfficeSettings.message/statusMessage) is derived from internal_reply_message (HTML-stripped). No row for a worker → Available / Disabled / no calendar events. auto_reply_status is matched case-insensitively against the real MS Graph lowercase values (disabled/alwaysEnabled/scheduled) — this PascalCase enum is documentation only.","properties":{"id":{"type":"string","readOnly":true,"example":"MSOOO-alice.smith0"},"worker_email":{"type":"string","format":"email"},"is_ooo":{"type":"boolean","example":true},"availability":{"type":"string","enum":["Available","Away","Busy","BeRightBack","DoNotDisturb","Offline","OutOfOffice"],"example":"Away"},"activity":{"type":"string","enum":["Available","Away","InACall","OutOfOffice","Presenting","DoNotDisturb","Busy","Offline"],"example":"OutOfOffice"},"auto_reply_status":{"type":"string","enum":["Disabled","AlwaysEnabled","Scheduled"],"example":"Scheduled"},"scheduled_start":{"type":"string","format":"date-time","nullable":true,"example":"2026-07-21T00:00:00"},"scheduled_end":{"type":"string","format":"date-time","nullable":true,"example":"2026-07-28T23:59:59"},"calendar_start":{"type":"string","format":"date-time","nullable":true,"example":"2026-07-21T08:00:00"},"calendar_end":{"type":"string","format":"date-time","nullable":true,"example":"2026-07-21T12:00:00"},"internal_reply_message":{"type":"string","example":"<html>I am out of office until July 28th. For urgent matters contact my manager.</html>"},"external_reply_message":{"type":"string","example":"I'm currently out of office with limited access to email. I'll respond when I'm back."},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"manager-experience.KbArticle":{"type":"object","description":"Data-layer storage shape for Manager_View_As_Reportee's search_articles_as_reportee action. The /api/sn_mgr_exp/manager_view_as/search contract response re-shapes this into the real captured field set (kbId, number, shortDescription, snippet, articles_markdown) — see contract.ts.","properties":{"id":{"type":"string","readOnly":true,"example":"KBART-0001"},"kbId":{"type":"string","example":"3f9c2a1e8b7d4650a1c2e3f4a5b6c7d8"},"number":{"type":"string","example":"KB1000001"},"title":{"type":"string","example":"Remote Work Guidelines"},"shortDescription":{"type":"string"},"text":{"type":"string"},"tags":{"type":"array","items":{"type":"string"},"example":["remote","policy"]},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"manager-experience.Employee":{"type":"object","description":"Eval-fixture roster override for Manager_View_As_Reportee / View_Holiday_List. Empty by default — when populated (via the generic bulk-import route), contract.ts's resolveEffectiveRoster (employeeOverride.ts) builds the effective roster from these rows instead of the random 20-person roster, so a scenario's exact named org chart (including a matrixManagerId dotted-line report) is what these 2 plugins answer against.","properties":{"id":{"type":"string","example":"EMP-101"},"firstName":{"type":"string"},"lastName":{"type":"string"},"email":{"type":"string","format":"email"},"managerId":{"type":"string","nullable":true,"example":"EMP-100"},"matrixManagerId":{"type":"string","nullable":true,"description":"Dotted-line manager id — counts as a direct report for orgDescendants alongside managerId."},"department":{"type":"string"},"title":{"type":"string"},"location":{"type":"string","example":"San Diego"},"country":{"type":"string","description":"Free text — not limited to the random roster's IN|AU|US.","example":"India"},"region":{"type":"string"},"businessUnit":{"type":"string"},"team":{"type":"string"},"employment_type":{"type":"string","description":"Free text (e.g. full_time, part_time, contractor, intern) — an eval fixture can set anything. Defaults to full_time if omitted.","example":"full_time"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"manager-experience.Holiday":{"type":"object","description":"Both this app's built-in recurring calendar rows (seeded by default: country/month/day, no year) AND any imported eval-fixture one-off rows (start_date/end_date) live in this one collection — holidays.ts's resolveEffectiveHolidays merges the two per country: a one-off row only overrides the recurring calendar on the specific date it lands on, so the rest of that country's recurring holidays still show. The /api/sn_mgr_exp/mw_holiday_api/holiday_list contract response expands either shape into concrete, lookahead-windowed dates with is_today.","properties":{"id":{"type":"string","example":"HOL-0001"},"name":{"type":"string","example":"Diwali"},"country":{"type":"string","description":"IN|AU|US for built-in rows; free text for imported fixture rows.","example":"India"},"month":{"type":"integer","minimum":1,"maximum":12,"nullable":true,"description":"Built-in recurring rows only."},"day":{"type":"integer","minimum":1,"maximum":31,"nullable":true,"description":"Built-in recurring rows only."},"start_date":{"type":"string","format":"date","nullable":true,"description":"Imported one-off fixture rows only."},"end_date":{"type":"string","format":"date","nullable":true,"description":"Imported one-off fixture rows only."},"date_display":{"type":"string","nullable":true,"example":"Sun, Nov 8 2026"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"pages.PageBlock":{"type":"object","properties":{"id":{"type":"string","description":"Block UUID","readOnly":true},"type":{"type":"string","enum":["heading","paragraph","bulletListItem","numberedListItem"],"example":"paragraph"},"props":{"type":"object","properties":{"textColor":{"type":"string","example":"default"},"backgroundColor":{"type":"string","example":"default"},"textAlignment":{"type":"string","enum":["left","center","right"],"example":"left"},"level":{"type":"integer","description":"Heading level (1–3), present only on heading blocks","example":1}}},"content":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","example":"text"},"text":{"type":"string","example":"Company OKRs — Q1 2024"},"styles":{"type":"object","additionalProperties":true}}}},"children":{"type":"array","items":{},"description":"Nested child blocks"}}},"pages.Page":{"type":"object","properties":{"id":{"type":"string","readOnly":true},"parent_id":{"type":"string","nullable":true,"example":null,"description":"ID of the parent page for nested page hierarchy; null for a top-level page. Reparent via POST /pages/{id}/move."},"title":{"type":"string","example":"Company OKRs Q1 2024"},"emoji":{"type":"string","example":"🎯"},"createdBy":{"type":"string","format":"email","example":"alice@acme.com"},"lastEditedAt":{"type":"string","format":"date-time","example":"2024-03-28T10:00:00.000Z","description":"ISO-8601 timestamp of the last edit; set by the client (not auto-managed by the server)"},"shared":{"type":"boolean","example":true},"sharedWith":{"type":"array","items":{"type":"string","format":"email"},"example":["bob@acme.com","carol@acme.com"]},"permissions":{"type":"string","enum":["view","comment","edit"],"example":"view"},"content":{"type":"array","items":{"$ref":"#/components/schemas/pages.PageBlock"},"description":"Rich-text block content of the page"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"pages.Database":{"type":"object","properties":{"id":{"type":"string","readOnly":true},"name":{"type":"string","example":"Project Tracker"},"createdBy":{"type":"string","format":"email","example":"alice@acme.com"},"viewType":{"type":"string","enum":["table","board","gallery","list","calendar"],"example":"table"},"schema":{"type":"array","items":{"type":"string"},"description":"Ordered list of column/property names","example":["Name","Status","Owner","Due Date","Priority"]},"shared":{"type":"boolean","example":true},"sharedWith":{"type":"array","items":{"type":"string","format":"email"},"example":["bob@acme.com","carol@acme.com"]},"permissions":{"type":"string","enum":["view","comment","edit"],"example":"edit"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"pages.DbRecord":{"type":"object","description":"A single row/record inside a Purple Pages database. The shape of user-defined fields (e.g. status, owner, priority) mirrors the database schema and varies per database type.","properties":{"id":{"type":"string","readOnly":true},"databaseId":{"type":"string","description":"ID of the parent Database document","example":"a1b2c3d4-e5f6-7890-abcd-ef1234567890"},"title":{"type":"string","description":"Primary title / name of the record","example":"Launch SSO Integration"},"status":{"type":"string","example":"In Progress"},"owner":{"type":"string","description":"Owner email or name (present on Project Tracker records)","example":"alice@acme.com"},"dueDate":{"type":"string","example":"2024-03-31"},"priority":{"type":"string","description":"Priority flag (present on Project Tracker records)","enum":["P0","P1","P2","P3"],"example":"P0"},"severity":{"type":"string","description":"Severity level (present on Bug Tracker records)","enum":["Critical","High","Medium","Low"],"example":"High"},"assignee":{"type":"string","description":"Assignee email (present on Bug Tracker records)","example":"bob@acme.com"},"component":{"type":"string","description":"Affected component (present on Bug Tracker records)","example":"Auth"},"author":{"type":"string","description":"Content author email (present on Content Calendar records)","example":"carol@acme.com"},"publishDate":{"type":"string","description":"Scheduled publish date (present on Content Calendar records)","example":"2024-04-01"},"channel":{"type":"string","description":"Distribution channel (present on Content Calendar records)","enum":["Blog","Email","LinkedIn","Twitter","Webinar"],"example":"Blog"},"category":{"type":"string","description":"Vendor category (present on Vendor Directory records)","example":"Cloud Infrastructure"},"contact":{"type":"string","description":"Vendor contact email (present on Vendor Directory records)","example":"support@cloudvendor.example"},"contractStatus":{"type":"string","description":"Contract status (present on Vendor Directory records)","enum":["Active","Pending","Expired"],"example":"Active"},"annualValue":{"type":"string","description":"Annual contract value string (present on Vendor Directory records)","example":"$180,000"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"pages.Comment":{"type":"object","description":"A comment attached to a page (Notion-style page comment).","properties":{"id":{"type":"string","readOnly":true},"page_id":{"type":"string","description":"ID of the page this comment is attached to","example":"a1b2c3d4-e5f6-7890-abcd-ef1234567890"},"discussion_id":{"type":"string","nullable":true,"description":"Discussion/thread this comment belongs to; null for a top-level page comment","example":null},"body":{"type":"string","description":"Comment text","example":"Left a note on the open questions section."},"createdBy":{"type":"string","format":"email","example":"alice@acme.com"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"pm.Project":{"type":"object","properties":{"id":{"type":"string","readOnly":true},"name":{"type":"string","example":"Platform Redesign"},"status":{"type":"string","enum":["planning","active","on_hold","completed","cancelled"],"example":"active"},"owner":{"type":"string","example":"alice@acme.com"},"startDate":{"type":"string","format":"date-time","example":"2026-03-01T00:00:00.000Z"},"endDate":{"type":"string","format":"date-time","example":"2026-06-30T00:00:00.000Z"},"progress":{"type":"integer","minimum":0,"maximum":100,"example":42},"budget":{"type":"number","example":150000},"initiativeIds":{"type":"array","items":{"type":"string"},"example":["INIT-001"],"description":"IDs of the initiatives this project rolls up to (Linear-style initiative→project hierarchy)"},"teamIds":{"type":"array","items":{"type":"string"},"example":["TEAM-001"],"description":"IDs of the teams that own this project"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"pm.Task":{"type":"object","properties":{"id":{"type":"string","readOnly":true},"title":{"type":"string","example":"Set up CI/CD pipeline"},"projectId":{"type":"string","example":"PROJ-001"},"status":{"type":"string","enum":["backlog","todo","in_progress","review","done","closed"],"example":"todo","description":"Current task status. 'closed' is a terminal state equivalent to 'done'; transitions to 'done' or 'closed' are blocked while openSubtasks > 0 or blockers is non-empty."},"assignee":{"type":"string","example":"alice@acme.com"},"priority":{"type":"string","enum":["low","medium","high","urgent"],"example":"high"},"storyPoints":{"type":"integer","example":5},"dueDate":{"type":"string","format":"date-time","example":"2026-06-15T00:00:00.000Z"},"sprintId":{"type":"string","nullable":true,"example":"SPR-001"},"dependencies":{"type":"array","items":{"type":"string"},"example":[],"description":"IDs of tasks this task depends on"},"labelIds":{"type":"array","items":{"type":"string"},"example":["LABEL-001"],"description":"IDs of labels applied to this task (issue)"},"teamId":{"type":"string","nullable":true,"example":"TEAM-001","description":"ID of the team that owns this task (issue)"},"openSubtasks":{"type":"integer","example":0,"description":"Number of open subtasks; task cannot be completed while > 0"},"blockers":{"type":"array","items":{"type":"string"},"example":[],"description":"Active blocker IDs; task cannot be completed while non-empty"},"reopenReason":{"type":"string","example":"Scope changed after cancellation","description":"Required when reopening a cancelled task"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"pm.Sprint":{"type":"object","properties":{"id":{"type":"string","readOnly":true},"name":{"type":"string","example":"Sprint 1"},"projectId":{"type":"string","example":"PROJ-001"},"status":{"type":"string","enum":["planned","active","completed"],"example":"planned"},"startDate":{"type":"string","format":"date-time","example":"2026-05-01T00:00:00.000Z"},"endDate":{"type":"string","format":"date-time","example":"2026-05-14T00:00:00.000Z"},"velocity":{"type":"integer","example":40},"completedPoints":{"type":"integer","example":34},"startedAt":{"type":"string","format":"date-time","readOnly":true},"completedAt":{"type":"string","format":"date-time","readOnly":true},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"pm.Milestone":{"type":"object","properties":{"id":{"type":"string","readOnly":true},"name":{"type":"string","example":"MVP Complete"},"projectId":{"type":"string","example":"PROJ-001"},"status":{"type":"string","enum":["upcoming","in_progress","completed","at_risk"],"example":"upcoming"},"dueDate":{"type":"string","format":"date-time","example":"2026-06-30T00:00:00.000Z"},"completedAt":{"type":"string","format":"date-time","nullable":true,"readOnly":true},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"pm.Initiative":{"type":"object","description":"A cross-project initiative (Linear-style); projects roll up to initiatives via Project.initiativeIds.","properties":{"id":{"type":"string","readOnly":true},"name":{"type":"string","example":"26Q2 Platform Investments"},"description":{"type":"string","example":"Cross-team platform work for 26Q2"},"status":{"type":"string","enum":["planned","active","completed"],"example":"active"},"owner":{"type":"string","format":"email","example":"alice@acme.com"},"targetDate":{"type":"string","format":"date-time","example":"2026-06-30T00:00:00.000Z"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"pm.Label":{"type":"object","description":"An issue label/tag applied to tasks via Task.labelIds.","properties":{"id":{"type":"string","readOnly":true},"name":{"type":"string","example":"p1"},"color":{"type":"string","example":"#e5484d"},"description":{"type":"string","example":"Highest priority"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"pm.User":{"type":"object","description":"A workspace member (issue assignee / project lead resolution).","properties":{"id":{"type":"string","readOnly":true},"name":{"type":"string","example":"Alice Chen"},"email":{"type":"string","format":"email","example":"alice@acme.com"},"role":{"type":"string","example":"engineer"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"pm.StatusUpdate":{"type":"object","description":"A progress/status update posted against a project or initiative.","properties":{"id":{"type":"string","readOnly":true},"projectId":{"type":"string","nullable":true,"example":"PROJ-001","description":"Project this update is about, if applicable"},"initiativeId":{"type":"string","nullable":true,"example":"INIT-001","description":"Initiative this update is about, if applicable"},"author":{"type":"string","format":"email","example":"alice@acme.com"},"body":{"type":"string","example":"Shipped the auth refactor; on track for the June milestone."},"health":{"type":"string","enum":["on_track","at_risk","off_track"],"example":"on_track"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"pm.Team":{"type":"object","description":"A team (Linear-style); projects/tasks are owned by a team via Project.teamIds / Task.teamId.","properties":{"id":{"type":"string","readOnly":true},"name":{"type":"string","example":"Platform"},"key":{"type":"string","example":"PLAT","description":"Short team key (e.g. used in issue identifiers like PLAT-42)"},"memberIds":{"type":"array","items":{"type":"string"},"example":["USER-001"],"description":"IDs of the users on this team"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"purple-talent.FieldTerm":{"type":"object","properties":{"term":{"type":"string"},"count":{"type":"integer"},"percentage":{"type":"number"}}},"purple-talent.CompanyRoleFlowHeadline":{"type":"object","properties":{"inflow_joined_in_window_count":{"type":"integer","example":8},"outflow_left_in_window_count":{"type":"integer","example":3},"net_window_flow":{"type":"integer","example":5}}},"purple-talent.Job":{"type":"object","properties":{"id":{"type":"integer"},"title":{"type":"string"},"organization":{"type":"object","properties":{"name":{"type":"string"}}},"work_mode":{"type":"string","enum":["remote","hybrid","onsite"]},"status":{"type":"string","enum":["active","closed"]}}},"retail-banking.Customer":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"CUST-0001"},"country":{"type":"string","enum":["IN","AU","US","GB","KR","DE"],"example":"IN"},"firstName":{"type":"string","example":"Ananya"},"lastName":{"type":"string","example":"Sharma"},"email":{"type":"string","format":"email"},"phone":{"type":"string","example":"+91 98765 43210"},"address":{"type":"string"},"city":{"type":"string","example":"Mumbai"},"segment":{"type":"string","enum":["mass","affluent","hni","nri","sme"],"example":"affluent"},"kyc_status":{"type":"string","enum":["verified","pending","expired","in_remediation"],"example":"verified"},"risk_rating":{"type":"string","enum":["low","medium","high"],"example":"low"},"relationship_manager":{"type":"string","format":"email"},"home_branch_id":{"type":"string","example":"BR-IN-014"},"tax_id":{"type":"object","properties":{"type":{"type":"string","example":"PAN"},"masked":{"type":"string","example":"ABCDE****F"}}},"credit_score":{"type":"object","properties":{"system":{"type":"string","example":"CIBIL"},"value":{"type":"integer","example":782}}},"onboarded_at":{"type":"string","format":"date-time"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"retail-banking.Account":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"ACCT-0001"},"customer_id":{"type":"string","example":"CUST-0001"},"country":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"type":{"type":"string","example":"Savings"},"currency":{"type":"string","example":"INR"},"balance":{"type":"object","properties":{"amount":{"type":"number","example":250000},"currency":{"type":"string","example":"INR"},"amount_formatted":{"type":"string","example":"₹2,50,000"}}},"status":{"type":"string","enum":["active","dormant","frozen","closed"],"example":"active"},"freeze_reason":{"type":"string","nullable":true},"branch_id":{"type":"string"},"routing":{"type":"object","properties":{"label":{"type":"string","example":"IFSC"},"code":{"type":"string","example":"HDFC0001234"},"accountNumber":{"type":"string"}}},"status_history":{"type":"array","readOnly":true,"items":{"type":"object","properties":{"status":{"type":"string"},"reason":{"type":"string","nullable":true},"recorded_at":{"type":"string","format":"date-time"}}}},"opened_at":{"type":"string","format":"date-time"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"retail-banking.Transaction":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"TXN-00001"},"account_id":{"type":"string","example":"ACCT-0001"},"customer_id":{"type":"string","example":"CUST-0001"},"country":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"amount":{"type":"object","properties":{"amount":{"type":"number","example":250000},"currency":{"type":"string","example":"INR"},"amount_formatted":{"type":"string","example":"₹2,50,000"}}},"direction":{"type":"string","enum":["debit","credit"],"example":"debit"},"channel":{"type":"string","example":"UPI"},"counterparty":{"type":"string","example":"Amazon Pay"},"status":{"type":"string","enum":["posted","pending","disputed","reversed"],"example":"posted"},"is_flagged":{"type":"boolean","example":false},"posted_at":{"type":"string","format":"date-time"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"retail-banking.Card":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"CARD-0001"},"customer_id":{"type":"string"},"account_id":{"type":"string"},"country":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"network":{"type":"string","enum":["visa","mastercard","rupay","amex"],"example":"rupay"},"type":{"type":"string","enum":["debit","credit"],"example":"debit"},"status":{"type":"string","enum":["active","blocked","expired"],"example":"active"},"last_four":{"type":"string","example":"4321"},"block_reason":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"retail-banking.Product":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"PROD-001"},"country":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"name":{"type":"string","example":"Tax-Saver Fixed Deposit"},"category":{"type":"string","enum":["deposit","lending","card","investment","insurance"],"example":"deposit"},"interest_rate":{"type":"number","example":7.1},"min_amount":{"type":"object","properties":{"amount":{"type":"number","example":250000},"currency":{"type":"string","example":"INR"},"amount_formatted":{"type":"string","example":"₹2,50,000"}}},"eligibility_summary":{"type":"string"},"active":{"type":"boolean","example":true},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"retail-banking.Loan":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"LOAN-0001"},"customer_id":{"type":"string","example":"CUST-0001"},"product_id":{"type":"string"},"country":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"type":{"type":"string","example":"home"},"principal":{"type":"object","properties":{"amount":{"type":"number","example":250000},"currency":{"type":"string","example":"INR"},"amount_formatted":{"type":"string","example":"₹2,50,000"}}},"outstanding":{"type":"object","properties":{"amount":{"type":"number","example":250000},"currency":{"type":"string","example":"INR"},"amount_formatted":{"type":"string","example":"₹2,50,000"}}},"interest_rate":{"type":"number","example":8.6},"term_months":{"type":"integer","example":240},"status":{"type":"string","enum":["active","delinquent","closed","written_off"],"example":"active"},"dpd":{"type":"integer","description":"Days past due","example":0},"delinquency_bucket":{"type":"string","enum":["current","1-30","31-60","61-90","90+"],"example":"current"},"next_due_date":{"type":"string","format":"date"},"repayment_schedule":{"type":"array","items":{"type":"object","properties":{"installment_no":{"type":"integer"},"due_date":{"type":"string","format":"date"},"amount":{"type":"object","properties":{"amount":{"type":"number","example":250000},"currency":{"type":"string","example":"INR"},"amount_formatted":{"type":"string","example":"₹2,50,000"}}},"status":{"type":"string","enum":["paid","due","overdue"]},"paid_at":{"type":"string","format":"date-time","nullable":true}}}},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"retail-banking.LoanApplication":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"LAPP-0001"},"customer_id":{"type":"string","example":"CUST-0001"},"product_id":{"type":"string"},"country":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"type":{"type":"string","example":"home"},"requested_amount":{"type":"object","properties":{"amount":{"type":"number","example":250000},"currency":{"type":"string","example":"INR"},"amount_formatted":{"type":"string","example":"₹2,50,000"}}},"purpose":{"type":"string"},"stage":{"type":"string","enum":["submitted","docs_pending","eligibility","underwriting","approved","rejected","disbursed"],"example":"docs_pending"},"required_documents":{"type":"array","items":{"type":"object","properties":{"doc_type":{"type":"string"},"status":{"type":"string","enum":["missing","received","verified"]}}}},"eligibility_score":{"type":"integer","example":71},"decision_reason":{"type":"string","nullable":true},"assigned_rm":{"type":"string","format":"email"},"stage_history":{"type":"array","readOnly":true,"items":{"type":"object","properties":{"stage":{"type":"string"},"reason":{"type":"string","nullable":true},"recorded_at":{"type":"string","format":"date-time"}}}},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"retail-banking.Interaction":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"INT-0001"},"customer_id":{"type":"string","example":"CUST-0001"},"country":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"type":{"type":"string","enum":["call","meeting","note","visit"],"example":"meeting"},"subject":{"type":"string","example":"FD maturity review"},"summary":{"type":"string"},"rm":{"type":"string","format":"email"},"occurred_at":{"type":"string","format":"date-time"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"retail-banking.Opportunity":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"OPP-0001"},"customer_id":{"type":"string","example":"CUST-0001"},"country":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"name":{"type":"string","example":"FD renewal → Tax-saver FD upsell"},"product_category":{"type":"string","enum":["deposit","lending","card","investment","insurance"],"example":"deposit"},"stage":{"type":"string","enum":["identified","proposed","accepted","declined"],"example":"identified"},"recommendation_reason":{"type":"string","example":"FD maturing in 12 days; eligible for higher tax-saver rate"},"value":{"type":"object","properties":{"amount":{"type":"number","example":250000},"currency":{"type":"string","example":"INR"},"amount_formatted":{"type":"string","example":"₹2,50,000"}}},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"retail-banking.Complaint":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"CMP-0001"},"customer_id":{"type":"string","example":"CUST-0001"},"account_id":{"type":"string","nullable":true},"country":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"category":{"type":"string","enum":["payments","cards","loans","accounts","atm","digital"],"example":"payments"},"channel":{"type":"string","enum":["branch","call_centre","mobile_app","email"],"example":"mobile_app"},"title":{"type":"string","example":"Unauthorised UPI transaction"},"status":{"type":"string","enum":["logged","investigating","resolved","closed"],"example":"logged"},"sla_policy_id":{"type":"string","nullable":true},"sla_due":{"type":"string","format":"date-time","nullable":true},"known_issue_id":{"type":"string","nullable":true},"resolution_notes":{"type":"string","nullable":true},"status_history":{"type":"array","readOnly":true,"items":{"type":"object","properties":{"status":{"type":"string"},"reason":{"type":"string","nullable":true},"recorded_at":{"type":"string","format":"date-time"}}}},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"retail-banking.KnownIssue":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"KI-001"},"country":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"title":{"type":"string","example":"UPI outage – NPCI"},"affected_service":{"type":"string","example":"UPI"},"status":{"type":"string","enum":["known_error","resolved"],"example":"known_error"},"workaround":{"type":"string"},"started_at":{"type":"string","format":"date-time"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"retail-banking.SlaPolicy":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"SLA-001"},"country":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"name":{"type":"string","example":"UPI Dispute Resolution SLA"},"category":{"type":"string","enum":["payments","cards","loans","accounts","atm","digital"],"example":"payments"},"response_hours":{"type":"number","example":4},"resolution_hours":{"type":"number","example":48},"regulatory_clock":{"type":"boolean","example":true},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"risk-compliance.KycProfile":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"KYC-0001"},"customer_id":{"type":"string","example":"CUST-0001"},"country":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"status":{"type":"string","enum":["verified","pending","expired","in_remediation"],"example":"pending"},"risk_level":{"type":"string","enum":["low","medium","high"],"example":"medium"},"last_reviewed_at":{"type":"string","format":"date-time"},"next_review_due":{"type":"string","format":"date"},"pep_flag":{"type":"boolean","example":false},"documents":{"type":"array","items":{"type":"object","properties":{"doc_type":{"type":"string"},"status":{"type":"string","enum":["valid","expired","missing","pending_review"]},"expires_at":{"type":"string","format":"date","nullable":true}}}},"outstanding_items":{"type":"array","items":{"type":"string"}},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"risk-compliance.RemediationCase":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"RMD-0001"},"customer_id":{"type":"string","example":"CUST-0001"},"kyc_profile_id":{"type":"string"},"country":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"reason":{"type":"string","enum":["expired_doc","re_kyc_nri","sanctions_hit","address_mismatch"],"example":"re_kyc_nri"},"status":{"type":"string","enum":["open","in_progress","pending_customer","completed","closed"],"example":"open"},"assigned_to":{"type":"string","format":"email"},"due_date":{"type":"string","format":"date"},"actions":{"type":"array","items":{"type":"object","properties":{"action":{"type":"string"},"owner":{"type":"string","nullable":true},"due":{"type":"string","nullable":true},"done":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"}}}},"status_history":{"type":"array","readOnly":true,"items":{"type":"object","properties":{"status":{"type":"string"},"note":{"type":"string","nullable":true},"recorded_at":{"type":"string","format":"date-time"}}}},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"risk-compliance.FraudAlert":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"FRAUD-0001"},"customer_id":{"type":"string","example":"CUST-0001"},"country":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"indicator":{"type":"string","enum":["velocity","geo_anomaly","unauthorized_upi","sim_swap","mule_pattern"],"example":"unauthorized_upi"},"severity":{"type":"string","enum":["low","medium","high","critical"],"example":"high"},"status":{"type":"string","enum":["new","triaged","dismissed","escalated"],"example":"new"},"score":{"type":"integer","example":82},"flagged_activity":{"type":"string"},"case_id":{"type":"string","nullable":true},"detected_at":{"type":"string","format":"date-time"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"risk-compliance.FraudCase":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"FC-0001"},"customer_id":{"type":"string","example":"CUST-0001"},"country":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"alert_ids":{"type":"array","items":{"type":"string"}},"status":{"type":"string","enum":["open","investigating","closed"],"example":"open"},"disposition":{"type":"string","enum":["confirmed_fraud","false_positive","pending"],"nullable":true},"risk_indicators":{"type":"array","items":{"type":"string"}},"assigned_investigator":{"type":"string","format":"email"},"case_notes":{"type":"array","readOnly":true,"items":{"type":"object","properties":{"note":{"type":"string"},"recorded_at":{"type":"string","format":"date-time"}}}},"status_history":{"type":"array","readOnly":true,"items":{"type":"object","properties":{"status":{"type":"string"},"note":{"type":"string","nullable":true},"recorded_at":{"type":"string","format":"date-time"}}}},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"risk-compliance.WatchlistEntry":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"WL-001"},"customer_id":{"type":"string","nullable":true},"country":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"name":{"type":"string"},"list_type":{"type":"string","enum":["sanctions","pep","internal_blocklist"],"example":"pep"},"match_strength":{"type":"integer","example":78},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"risk-compliance.Policy":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"POL-001"},"country":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"title":{"type":"string","example":"Re-KYC procedure for NRI accounts"},"policy_type":{"type":"string","enum":["SOP","policy","procedure","customer_script"],"example":"procedure"},"category":{"type":"string","enum":["KYC","payments","lending","branch","fraud"],"example":"KYC"},"body":{"type":"string"},"customer_script":{"type":"string","description":"Verbatim text the frontline staff can read to the customer"},"regulator":{"type":"string","example":"RBI"},"effective_date":{"type":"string","format":"date"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"scm.Supplier":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"SUP-0001"},"name":{"type":"string","example":"Global Supply Co"},"status":{"type":"string","enum":["active","inactive","pending_approval","blacklisted"],"example":"active"},"category":{"type":"string","enum":["Raw Materials","Electronics","Office Supplies","Services","Packaging"],"example":"Electronics"},"country":{"type":"string","example":"USA"},"rating":{"type":"integer","minimum":1,"maximum":5,"example":4},"onboardedAt":{"type":"string","format":"date-time","example":"2025-05-01T00:00:00.000Z"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"scm.Contract":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"CON-0001"},"supplierId":{"type":"string","example":"SUP-0001"},"title":{"type":"string","example":"Master Supply Agreement - 2025"},"status":{"type":"string","enum":["draft","active","signed","expired","cancelled"],"example":"active"},"value":{"type":"number","example":250000},"startDate":{"type":"string","format":"date-time","example":"2025-01-01T00:00:00.000Z"},"endDate":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"},"autoRenew":{"type":"boolean","example":true},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"scm.Shipment":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"SHIP-0001"},"supplierId":{"type":"string","example":"SUP-0001"},"status":{"type":"string","enum":["pending","in_transit","out_for_delivery","delivered","exception","cancelled"],"example":"in_transit"},"carrier":{"type":"string","enum":["FedEx","UPS","DHL","USPS","Maersk","MSC"],"example":"FedEx"},"trackingNumber":{"type":"string","example":"1Z999AA10123456784"},"origin":{"type":"string","example":"Shanghai"},"destination":{"type":"string","example":"New York"},"estimatedDelivery":{"type":"string","format":"date-time","example":"2026-06-05T00:00:00.000Z"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"scm.PurchaseOrder":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"SCMPO-0001"},"supplierId":{"type":"string","example":"SUP-0001"},"status":{"type":"string","enum":["draft","submitted","approved","received","cancelled"],"example":"draft"},"totalAmount":{"type":"number","example":45000},"lineItems":{"type":"integer","example":5},"deliveryDate":{"type":"string","format":"date-time","example":"2026-06-15T00:00:00.000Z"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"scm.QualityLot":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"QL-0001"},"supplierId":{"type":"string","example":"SUP-0001"},"itemSku":{"type":"string","example":"SKU-A1B2C3D4"},"quantity":{"type":"integer","example":500},"status":{"type":"string","enum":["pending","in_inspection","passed","failed","quarantined"],"example":"pending"},"defectRate":{"type":"number","format":"float","example":1.25},"inspectedBy":{"type":"string","example":"qa1@company.com"},"receivedAt":{"type":"string","format":"date-time","example":"2026-05-15T00:00:00.000Z"},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"tasks-approvals.Task":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"TASK-0001"},"title":{"type":"string","example":"Complete benefits enrollment"},"description":{"type":"string","example":"Review and confirm your benefits elections for this plan year."},"assignee_email":{"type":"string","format":"email","example":"abraham.lincoln@acme.com"},"delegated_to_email":{"type":"string","format":"email","nullable":true,"example":null},"task_type_key":{"type":"string","enum":["hr_tasks","universal_task","document_tasks","legal_hold_tasks","learning_tasks","time_off_request_todos","leave_of_absence_request_todos","termination_request_todos","job_change_request_todos","compensation_change_request_todos","timesheet_request_todos","spend_authorization_request_todos","invoice","milestone","receipt","sourcing","mentorship_requests","post_feedback","job_requisition_request_todos","correct_time_off_request_todos","hr_acceptance","journey_accelerator_to_dos","cd_task","supplier_task","actions","procurement_tasks"],"example":"hr_tasks"},"sub_type_name":{"type":"string","example":""},"status":{"type":"string","enum":["pending","completed","cancelled"],"example":"pending"},"priority":{"type":"string","enum":["critical","high","medium","low"],"nullable":true,"example":"high"},"due_date":{"type":"string","format":"date-time","nullable":true,"example":"2026-07-22T00:00:00.000Z"},"requester_name":{"type":"string","example":""},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"tasks-approvals.Approval":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"APR-0001"},"title":{"type":"string","example":"Contract review — vendor MSA"},"requester_name":{"type":"string","example":"Jordan Reyes"},"approver_email":{"type":"string","format":"email","example":"jordan.reyes@acme.com"},"delegated_to_email":{"type":"string","format":"email","nullable":true,"example":null},"task_type_key":{"type":"string","enum":["approvals","concur_expense_report_approval"],"example":"approvals"},"sub_type_name":{"type":"string","example":"Legal Matter"},"status":{"type":"string","enum":["pending","approved","rejected"],"example":"pending"},"priority":{"type":"string","enum":["critical","high","medium","low"],"nullable":true,"example":"high"},"due_date":{"type":"string","format":"date-time","nullable":true},"received_date":{"type":"string","format":"date","example":"2026-07-15"},"summary":{"type":"string","example":"$18,400 vendor procurement request"},"approval_decision":{"type":"object","readOnly":true,"nullable":true,"properties":{"decided_by":{"type":"string"},"decided_at":{"type":"string","format":"date-time"},"reason":{"type":"string"}}},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"tasks-approvals.TodosConfigResult":{"type":"object","properties":{"result":{"type":"object","properties":{"tabs":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"label":{"type":"string"}}}},"filters":{"type":"array","items":{"type":"object","properties":{"title":{"type":"string"},"categoryId":{"type":"string"},"filters":{"type":"array","items":{"type":"object","properties":{"filterName":{"type":"string"},"filterId":{"type":"string"},"isGroup":{"type":"boolean"},"filterDescription":{"type":"string"}}}}}}}}}}},"tasks-approvals.TodosResult":{"type":"object","properties":{"result":{"type":"object","properties":{"todos":{"type":"array","items":{"type":"object"}},"todoSysIds":{"type":"array","items":{"type":"string"}},"count":{"type":"integer"}}}}}}},"paths":{"/purple-suite/ats/requisitions":{"get":{"summary":"List Purple ATS job requisitions","description":"Approved openings to hire against in Purple ATS — title, department, headcount, hiring manager, required skills and status (open → in_progress/on_hold → filled/closed/cancelled). Candidates and job postings link back to a requisition by requisitionId. Supports server-side querying with _filter/_search/_orderby/_select/_top/_skip/_count (OData $-params, renamed with a leading underscore for MCP).","parameters":[{"$ref":"#/components/parameters/ats.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ats.Requisition"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["ats"]},"post":{"summary":"Open a job requisition","description":"Creates a new requisition (a request to hire) in Purple ATS. This opens headcount only — it does not publish an advert (create a job posting for that) and does not add applicants.","parameters":[{"$ref":"#/components/parameters/ats.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ats.Requisition"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ats.Requisition"}}}}},"tags":["ats"]}},"/purple-suite/ats/requisitions/{id}":{"get":{"summary":"Get requisition","parameters":[{"$ref":"#/components/parameters/ats.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ats.Requisition"}}}},"404":{"description":"Not found"}},"tags":["ats"]},"patch":{"summary":"Update requisition","parameters":[{"$ref":"#/components/parameters/ats.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ats.Requisition"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ats.Requisition"}}}},"404":{"description":"Not found"}},"tags":["ats"]},"delete":{"summary":"Delete requisition","parameters":[{"$ref":"#/components/parameters/ats.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["ats"]}},"/purple-suite/ats/candidates":{"get":{"summary":"List Purple ATS job applicants","description":"External people who applied to a Purple ATS requisition, with their pipeline stage (applied → screening → phone_screen → interview → offer → hired, or rejected/withdrawn), source and resume link. These are applicants, not employees (see the hris app) or chat/identity users. Supports server-side querying with _filter/_search/_orderby/_select/_top/_skip/_count (OData $-params, renamed with a leading underscore for MCP).","parameters":[{"$ref":"#/components/parameters/ats.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ats.Candidate"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["ats"]},"post":{"summary":"Add an applicant to the hiring pipeline","description":"Registers a new candidate against a Purple ATS requisition, starting at the 'applied' stage unless a status is supplied.","parameters":[{"$ref":"#/components/parameters/ats.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ats.Candidate"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ats.Candidate"}}}}},"tags":["ats"]}},"/purple-suite/ats/candidates/{id}":{"get":{"summary":"Get candidate","parameters":[{"$ref":"#/components/parameters/ats.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ats.Candidate"}}}},"404":{"description":"Not found"}},"tags":["ats"]},"patch":{"summary":"Update a candidate record","description":"Partially updates an applicant in Purple ATS (contact details, source, resume). This is also how a candidate is rejected or withdrawn — set status accordingly. Forward pipeline moves should use the candidate advance tool, which validates the transition and records stage history.","parameters":[{"$ref":"#/components/parameters/ats.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ats.Candidate"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ats.Candidate"}}}},"404":{"description":"Not found"}},"tags":["ats"]},"delete":{"summary":"Delete candidate","parameters":[{"$ref":"#/components/parameters/ats.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["ats"]}},"/purple-suite/ats/candidates/{id}/advance":{"post":{"summary":"Advance a candidate to the next hiring stage","description":"Moves a Purple ATS candidate forward through applied → screening → phone_screen → interview → offer → hired (auto-picks the next stage when no target is given) and appends an entry to stageHistory. Forward-only: it refuses backwards moves and candidates who are rejected, withdrawn or already hired — use the candidate update tool to reject or withdraw someone.","parameters":[{"$ref":"#/components/parameters/ats.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["applied","screening","phone_screen","interview","offer","hired"],"description":"Target stage to advance to. Omit to auto-advance to the next stage. Alias: stage (snake_case/legacy accepted).","example":"interview"},"notes":{"type":"string","nullable":true,"example":"Passed phone screen with flying colors."}}}}}},"responses":{"200":{"description":"Candidate advanced","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ats.Candidate"}}}},"400":{"description":"Invalid stage value"},"404":{"description":"Candidate not found"},"422":{"description":"Stage transition not allowed (rejected/withdrawn/hired/backwards move)"}},"tags":["ats"]}},"/purple-suite/ats/candidates/{id}/generate-offer":{"post":{"summary":"Generate a draft offer for a candidate","description":"Creates an Offer record in draft status for a Purple ATS candidate and moves that candidate to the 'offer' stage in one step — prefer this over creating an offer record directly. Rejected if the candidate is already hired or already has a draft/sent offer.","parameters":[{"$ref":"#/components/parameters/ats.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"salary":{"type":"number","nullable":true,"example":130000,"description":"Base salary. Alias: baseSalary (camelCase accepted). Stored as baseSalary on the created Offer document."},"equity":{"type":"string","nullable":true,"example":"0.25%","description":"Equity grant string (e.g. '0.25%')."},"start_date":{"type":"string","format":"date-time","nullable":true,"example":"2026-07-14T00:00:00.000Z","description":"Proposed start date. Alias: startDate (camelCase accepted)."},"expires_at":{"type":"string","format":"date-time","nullable":true,"example":"2026-06-20T00:00:00.000Z","description":"Offer expiry date. Alias: expiresAt (camelCase accepted)."},"sentAt":{"type":"string","format":"date-time","nullable":true,"example":"2026-06-01T00:00:00.000Z","description":"Timestamp when offer was sent, if already dispatched."}}}}}},"responses":{"201":{"description":"Offer created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ats.Offer"}}}},"404":{"description":"Candidate not found"},"422":{"description":"Candidate already hired or already has an active offer"}},"tags":["ats"]}},"/purple-suite/ats/interviews":{"get":{"summary":"List Purple ATS candidate interviews","description":"Interview sessions booked or completed for Purple ATS candidates — interviewer email, type (phone_screen | technical | behavioral | panel | final), scheduledAt, status and the written feedback plus 1–5 rating. These are hiring loops, not general calendar meetings (see the calendar app). Supports server-side querying with _filter/_search/_orderby/_select/_top/_skip/_count (OData $-params, renamed with a leading underscore for MCP).","parameters":[{"$ref":"#/components/parameters/ats.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ats.Interview"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["ats"]},"post":{"summary":"Create interview","parameters":[{"$ref":"#/components/parameters/ats.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ats.Interview"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ats.Interview"}}}}},"tags":["ats"]}},"/purple-suite/ats/interviews/{id}":{"get":{"summary":"Get interview","parameters":[{"$ref":"#/components/parameters/ats.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ats.Interview"}}}},"404":{"description":"Not found"}},"tags":["ats"]},"patch":{"summary":"Update interview","parameters":[{"$ref":"#/components/parameters/ats.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ats.Interview"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ats.Interview"}}}},"404":{"description":"Not found"}},"tags":["ats"]},"delete":{"summary":"Delete interview","parameters":[{"$ref":"#/components/parameters/ats.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["ats"]}},"/purple-suite/ats/offers":{"get":{"summary":"List Purple ATS job offers","description":"Employment offers extended to Purple ATS candidates — base salary, equity, start date, expiry and status (draft → sent → accepted/declined/expired/withdrawn). Check here for an existing draft/sent offer before generating another for the same candidate. Supports server-side querying with _filter/_search/_orderby/_select/_top/_skip/_count (OData $-params, renamed with a leading underscore for MCP).","parameters":[{"$ref":"#/components/parameters/ats.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ats.Offer"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["ats"]},"post":{"summary":"Create offer","parameters":[{"$ref":"#/components/parameters/ats.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ats.Offer"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ats.Offer"}}}}},"tags":["ats"]}},"/purple-suite/ats/offers/{id}":{"get":{"summary":"Get offer","parameters":[{"$ref":"#/components/parameters/ats.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ats.Offer"}}}},"404":{"description":"Not found"}},"tags":["ats"]},"patch":{"summary":"Update offer","parameters":[{"$ref":"#/components/parameters/ats.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ats.Offer"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ats.Offer"}}}},"404":{"description":"Not found"}},"tags":["ats"]},"delete":{"summary":"Delete offer","parameters":[{"$ref":"#/components/parameters/ats.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["ats"]}},"/purple-suite/ats/job-postings":{"get":{"summary":"List Purple ATS job postings","description":"Externally-facing job adverts in Purple ATS — title, location, employment type, applicant count and status (draft | active | paused | closed) — usually linked to a requisition. The posting is the advert; the requisition is the approved headcount behind it. Supports server-side querying with _filter/_search/_orderby/_select/_top/_skip/_count (OData $-params, renamed with a leading underscore for MCP).","parameters":[{"$ref":"#/components/parameters/ats.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ats.JobPosting"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["ats"]},"post":{"summary":"Create job posting","parameters":[{"$ref":"#/components/parameters/ats.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ats.JobPosting"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ats.JobPosting"}}}}},"tags":["ats"]}},"/purple-suite/ats/job-postings/{id}":{"get":{"summary":"Get job posting","parameters":[{"$ref":"#/components/parameters/ats.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ats.JobPosting"}}}},"404":{"description":"Not found"}},"tags":["ats"]},"patch":{"summary":"Update job posting","parameters":[{"$ref":"#/components/parameters/ats.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ats.JobPosting"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ats.JobPosting"}}}},"404":{"description":"Not found"}},"tags":["ats"]},"delete":{"summary":"Delete job posting","parameters":[{"$ref":"#/components/parameters/ats.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["ats"]}},"/purple-suite/ats/referrals":{"get":{"x-odata":false,"summary":"List Purple ATS employee referrals","description":"Candidates put forward by existing employees, with a per-status tally. Each referral records who referred whom (referrer_email → candidate_name/candidate_email), the position, and its status (pending | reviewing | hired | rejected). A referral is a lead, not yet a candidate in the pipeline — see ats__list__candidates for candidates under active consideration. Returns the whole collection with counts; it does not take OData query params.","parameters":[{"$ref":"#/components/parameters/ats.InstanceId"}],"responses":{"200":{"description":"Referral list","content":{"application/json":{"schema":{"type":"object","properties":{"referrals":{"type":"array","items":{"type":"object"}},"totalCount":{"type":"integer"},"statusCounts":{"type":"object"}}}}}}},"tags":["ats"]},"post":{"summary":"Submit an employee referral","description":"Records a candidate referred by an existing employee and stores it in the instance's referrals collection with status `pending`. referrer_email, candidate_name and candidate_email are required. This creates a referral lead only — it does not add anyone to the hiring pipeline; use ats__create__candidates for that.","parameters":[{"$ref":"#/components/parameters/ats.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"referrer_email":{"type":"string","format":"email","example":"employee@example.com"},"candidate_name":{"type":"string","example":"Jane Smith"},"candidate_email":{"type":"string","format":"email","example":"jane.smith@email.com"},"position":{"type":"string","example":"Software Engineer"}},"required":["referrer_email","candidate_name","candidate_email"]}}}},"responses":{"200":{"description":"Referral submitted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"referral":{"type":"object","properties":{"id":{"type":"string"},"referrer_email":{"type":"string"},"candidate_name":{"type":"string"},"candidate_email":{"type":"string"},"position":{"type":"string"},"status":{"type":"string"},"created_at":{"type":"string","format":"date-time"}}}}}}}},"400":{"description":"Missing required fields"}},"tags":["ats"]}},"/purple-suite/bank-ops/branches":{"get":{"summary":"List retail bank branches","parameters":[{"$ref":"#/components/parameters/bank-ops.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Max items per page (default 50)"},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Pass the previous response's nextCursor to fetch the next page"},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."},{"in":"query","name":"country","required":false,"schema":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"description":"Filter to a single country (IN | AU | US)"},{"in":"query","name":"status","required":false,"schema":{"type":"string","enum":["open","closed_today","limited"]},"description":"Filter by status"},{"in":"query","name":"region","required":false,"schema":{"type":"string"},"description":"Filter by region"}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/bank-ops.Branch"}},"nextCursor":{"type":"string","nullable":true},"total":{"type":"integer"}}}}}}},"description":"Physical branches of the Purple Bank Ops bank — name, city, region, country (IN | AU | US | GB | KR | DE), today's opening status and branch manager. Branch ids (BR-###) key this app's checklists, approvals, escalations and operational risks. Supports server-side querying with _filter/_search/_orderby/_select/_top/_skip/_count (OData $-params, renamed with a leading underscore for MCP), plus this tool's simple equality params (country, branch_id, status, …).","tags":["bank-ops"]},"post":{"summary":"Create branch","parameters":[{"$ref":"#/components/parameters/bank-ops.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/bank-ops.Branch"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/bank-ops.Branch"}}}}},"tags":["bank-ops"]}},"/purple-suite/bank-ops/branches/{id}":{"get":{"summary":"Get branch","parameters":[{"$ref":"#/components/parameters/bank-ops.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/bank-ops.Branch"}}}},"404":{"description":"Not found"}},"tags":["bank-ops"]},"patch":{"summary":"Update branch","parameters":[{"$ref":"#/components/parameters/bank-ops.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/bank-ops.Branch"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/bank-ops.Branch"}}}},"404":{"description":"Not found"}},"tags":["bank-ops"]},"delete":{"summary":"Delete branch","parameters":[{"$ref":"#/components/parameters/bank-ops.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"id":{"type":"string"}}}}}},"404":{"description":"Not found"}},"tags":["bank-ops"]}},"/purple-suite/bank-ops/branches/{id}/control-tower":{"get":{"summary":"Get a branch's control-tower briefing","description":"One call that assembles a single Purple Bank Ops branch's operational picture: today's opening checklist, approvals still pending, unresolved escalations, and operational risks due today. Prefer this over separately listing each collection when the question is about one branch (BR-### id).","parameters":[{"$ref":"#/components/parameters/bank-ops.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Aggregated control tower","content":{"application/json":{"schema":{"type":"object","properties":{"branch":{"$ref":"#/components/schemas/bank-ops.Branch"},"todays_checklist":{"$ref":"#/components/schemas/bank-ops.BranchChecklist"},"pending_approvals":{"type":"array","items":{"$ref":"#/components/schemas/bank-ops.BranchApproval"}},"open_escalations":{"type":"array","items":{"$ref":"#/components/schemas/bank-ops.Escalation"}},"operational_risks_due_today":{"type":"array","items":{"$ref":"#/components/schemas/bank-ops.OperationalRisk"}}}}}}},"404":{"description":"Branch not found"}},"tags":["bank-ops"]}},"/purple-suite/bank-ops/branch_checklists":{"get":{"summary":"List daily branch-opening checklists","parameters":[{"$ref":"#/components/parameters/bank-ops.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Max items per page (default 50)"},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Pass the previous response's nextCursor to fetch the next page"},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."},{"in":"query","name":"country","required":false,"schema":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"description":"Filter to a single country (IN | AU | US)"},{"in":"query","name":"branch_id","required":false,"schema":{"type":"string"},"description":"Filter by branch id"},{"in":"query","name":"overall_status","required":false,"schema":{"type":"string","enum":["pending","complete"]},"description":"Filter by overall status"}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/bank-ops.BranchChecklist"}},"nextCursor":{"type":"string","nullable":true},"total":{"type":"integer"}}}}}}},"description":"Branch-opening checklists in Purple Bank Ops — one per branch per date, each holding line items (cash count, vault, ATM, …) in pending | done | blocked plus a rolled-up overall_status. Tick individual items off with the checklist complete-item tool rather than patching the record. Supports server-side querying with _filter/_search/_orderby/_select/_top/_skip/_count (OData $-params, renamed with a leading underscore for MCP), plus this tool's simple equality params (country, branch_id, status, …).","tags":["bank-ops"]},"post":{"summary":"Create branch checklist","parameters":[{"$ref":"#/components/parameters/bank-ops.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/bank-ops.BranchChecklist"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/bank-ops.BranchChecklist"}}}}},"tags":["bank-ops"]}},"/purple-suite/bank-ops/branch_checklists/{id}":{"get":{"summary":"Get branch checklist","parameters":[{"$ref":"#/components/parameters/bank-ops.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/bank-ops.BranchChecklist"}}}},"404":{"description":"Not found"}},"tags":["bank-ops"]},"patch":{"summary":"Update branch checklist","parameters":[{"$ref":"#/components/parameters/bank-ops.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/bank-ops.BranchChecklist"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/bank-ops.BranchChecklist"}}}},"404":{"description":"Not found"}},"tags":["bank-ops"]},"delete":{"summary":"Delete branch checklist","parameters":[{"$ref":"#/components/parameters/bank-ops.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"id":{"type":"string"}}}}}},"404":{"description":"Not found"}},"tags":["bank-ops"]}},"/purple-suite/bank-ops/branch_checklists/{id}/complete-item":{"post":{"summary":"Complete (or block) a checklist item","description":"Sets a checklist item to done | blocked | pending and recomputes overall_status.","parameters":[{"$ref":"#/components/parameters/bank-ops.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"item_id":{"type":"string","example":"cash_count"},"status":{"type":"string","enum":["pending","done","blocked"]},"completed_by":{"type":"string"}},"required":["item_id","status"]}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/bank-ops.BranchChecklist"}}}},"400":{"description":"Invalid body"},"404":{"description":"Not found"}},"tags":["bank-ops"]}},"/purple-suite/bank-ops/branch_approvals":{"get":{"summary":"List branch approval requests","parameters":[{"$ref":"#/components/parameters/bank-ops.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Max items per page (default 50)"},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Pass the previous response's nextCursor to fetch the next page"},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."},{"in":"query","name":"country","required":false,"schema":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"description":"Filter to a single country (IN | AU | US)"},{"in":"query","name":"branch_id","required":false,"schema":{"type":"string"},"description":"Filter by branch id"},{"in":"query","name":"type","required":false,"schema":{"type":"string","enum":["limit_override","fee_waiver","large_withdrawal","loan_exception"]},"description":"Filter by type"},{"in":"query","name":"status","required":false,"schema":{"type":"string","enum":["pending","approved","rejected"]},"description":"Filter by status"}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/bank-ops.BranchApproval"}},"nextCursor":{"type":"string","nullable":true},"total":{"type":"integer"}}}}}}},"description":"Decisions a branch needs from an approver in Purple Bank Ops — limit overrides, fee waivers, large withdrawals and loan exceptions, with the requester, amount and status (pending | approved | rejected). These are banking operational approvals, not IT or HR approvals (see the itsm and hrbp apps). Supports server-side querying with _filter/_search/_orderby/_select/_top/_skip/_count (OData $-params, renamed with a leading underscore for MCP), plus this tool's simple equality params (country, branch_id, status, …).","tags":["bank-ops"]},"post":{"summary":"Create branch approval","parameters":[{"$ref":"#/components/parameters/bank-ops.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/bank-ops.BranchApproval"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/bank-ops.BranchApproval"}}}}},"tags":["bank-ops"]}},"/purple-suite/bank-ops/branch_approvals/{id}":{"get":{"summary":"Get branch approval","parameters":[{"$ref":"#/components/parameters/bank-ops.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/bank-ops.BranchApproval"}}}},"404":{"description":"Not found"}},"tags":["bank-ops"]},"patch":{"summary":"Update branch approval","parameters":[{"$ref":"#/components/parameters/bank-ops.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/bank-ops.BranchApproval"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/bank-ops.BranchApproval"}}}},"404":{"description":"Not found"}},"tags":["bank-ops"]},"delete":{"summary":"Delete branch approval","parameters":[{"$ref":"#/components/parameters/bank-ops.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"id":{"type":"string"}}}}}},"404":{"description":"Not found"}},"tags":["bank-ops"]}},"/purple-suite/bank-ops/branch_approvals/{id}/transition":{"post":{"summary":"Approve or reject a pending approval","description":"Sets a pending approval to approved | rejected. Already-decided approvals cannot be changed.","parameters":[{"$ref":"#/components/parameters/bank-ops.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["approved","rejected"]},"reason":{"type":"string"}},"required":["status"]}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/bank-ops.BranchApproval"}}}},"400":{"description":"Invalid status"},"404":{"description":"Not found"},"422":{"description":"Already decided"}},"tags":["bank-ops"]}},"/purple-suite/bank-ops/escalations":{"get":{"summary":"List branch escalations","parameters":[{"$ref":"#/components/parameters/bank-ops.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Max items per page (default 50)"},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Pass the previous response's nextCursor to fetch the next page"},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."},{"in":"query","name":"country","required":false,"schema":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"description":"Filter to a single country (IN | AU | US)"},{"in":"query","name":"branch_id","required":false,"schema":{"type":"string"},"description":"Filter by branch id"},{"in":"query","name":"category","required":false,"schema":{"type":"string","enum":["service","fraud","system_outage","compliance"]},"description":"Filter by category"},{"in":"query","name":"severity","required":false,"schema":{"type":"string","enum":["low","medium","high","critical"]},"description":"Filter by severity"},{"in":"query","name":"status","required":false,"schema":{"type":"string","enum":["open","acknowledged","resolved"]},"description":"Filter by status"}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/bank-ops.Escalation"}},"nextCursor":{"type":"string","nullable":true},"total":{"type":"integer"}}}}}}},"description":"Operational issues raised by a branch in Purple Bank Ops — service, fraud, system_outage or compliance — with severity, owner, a summary, an optional linked fraud/complaint reference, and status open → acknowledged → resolved. Supports server-side querying with _filter/_search/_orderby/_select/_top/_skip/_count (OData $-params, renamed with a leading underscore for MCP), plus this tool's simple equality params (country, branch_id, status, …).","tags":["bank-ops"]},"post":{"summary":"Create escalation","parameters":[{"$ref":"#/components/parameters/bank-ops.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/bank-ops.Escalation"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/bank-ops.Escalation"}}}}},"tags":["bank-ops"]}},"/purple-suite/bank-ops/escalations/{id}":{"get":{"summary":"Get escalation","parameters":[{"$ref":"#/components/parameters/bank-ops.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/bank-ops.Escalation"}}}},"404":{"description":"Not found"}},"tags":["bank-ops"]},"patch":{"summary":"Update escalation","parameters":[{"$ref":"#/components/parameters/bank-ops.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/bank-ops.Escalation"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/bank-ops.Escalation"}}}},"404":{"description":"Not found"}},"tags":["bank-ops"]},"delete":{"summary":"Delete escalation","parameters":[{"$ref":"#/components/parameters/bank-ops.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"id":{"type":"string"}}}}}},"404":{"description":"Not found"}},"tags":["bank-ops"]}},"/purple-suite/bank-ops/escalations/{id}/transition":{"post":{"summary":"Transition an escalation","description":"Moves an escalation through open → acknowledged → resolved. Resolved is terminal.","parameters":[{"$ref":"#/components/parameters/bank-ops.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["open","acknowledged","resolved"]},"owner":{"type":"string"}},"required":["status"]}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/bank-ops.Escalation"}}}},"400":{"description":"Invalid status"},"404":{"description":"Not found"},"422":{"description":"Already resolved"}},"tags":["bank-ops"]}},"/purple-suite/bank-ops/operational_risks":{"get":{"summary":"List the branch operational-risk register","parameters":[{"$ref":"#/components/parameters/bank-ops.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Max items per page (default 50)"},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Pass the previous response's nextCursor to fetch the next page"},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."},{"in":"query","name":"country","required":false,"schema":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"description":"Filter to a single country (IN | AU | US)"},{"in":"query","name":"branch_id","required":false,"schema":{"type":"string"},"description":"Filter by branch id"},{"in":"query","name":"risk_type","required":false,"schema":{"type":"string","enum":["cash_shortage","sla_breach","staffing","control_gap"]},"description":"Filter by risk type"},{"in":"query","name":"status","required":false,"schema":{"type":"string","enum":["open","mitigating","closed"]},"description":"Filter by status"}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/bank-ops.OperationalRisk"}},"nextCursor":{"type":"string","nullable":true},"total":{"type":"integer"}}}}}}},"description":"Branch-level op-risk entries in Purple Bank Ops — cash_shortage, sla_breach, staffing or control_gap — with severity, mitigation plan, a due_today flag and status open → mitigating → closed. This is day-to-day branch risk, distinct from the enterprise risk-compliance app. Supports server-side querying with _filter/_search/_orderby/_select/_top/_skip/_count (OData $-params, renamed with a leading underscore for MCP), plus this tool's simple equality params (country, branch_id, status, …).","tags":["bank-ops"]},"post":{"summary":"Create operational risk","parameters":[{"$ref":"#/components/parameters/bank-ops.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/bank-ops.OperationalRisk"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/bank-ops.OperationalRisk"}}}}},"tags":["bank-ops"]}},"/purple-suite/bank-ops/operational_risks/{id}":{"get":{"summary":"Get operational risk","parameters":[{"$ref":"#/components/parameters/bank-ops.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/bank-ops.OperationalRisk"}}}},"404":{"description":"Not found"}},"tags":["bank-ops"]},"patch":{"summary":"Update operational risk","parameters":[{"$ref":"#/components/parameters/bank-ops.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/bank-ops.OperationalRisk"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/bank-ops.OperationalRisk"}}}},"404":{"description":"Not found"}},"tags":["bank-ops"]},"delete":{"summary":"Delete operational risk","parameters":[{"$ref":"#/components/parameters/bank-ops.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"id":{"type":"string"}}}}}},"404":{"description":"Not found"}},"tags":["bank-ops"]}},"/purple-suite/bank-ops/operational_risks/{id}/transition":{"post":{"summary":"Transition an operational risk","description":"Moves a risk through open → mitigating → closed. Closing clears due_today. Closed is terminal.","parameters":[{"$ref":"#/components/parameters/bank-ops.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["open","mitigating","closed"]},"mitigation_plan":{"type":"string"}},"required":["status"]}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/bank-ops.OperationalRisk"}}}},"400":{"description":"Invalid status"},"404":{"description":"Not found"},"422":{"description":"Already closed"}},"tags":["bank-ops"]}},"/purple-suite/bank-ops/letters_of_credit":{"get":{"summary":"List trade-finance letters of credit","parameters":[{"$ref":"#/components/parameters/bank-ops.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Max items per page (default 50)"},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Pass the previous response's nextCursor to fetch the next page"},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."},{"in":"query","name":"country","required":false,"schema":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"description":"Filter to a single country (IN | AU | US)"},{"in":"query","name":"customer_id","required":false,"schema":{"type":"string"},"description":"Filter by customer id"},{"in":"query","name":"status","required":false,"schema":{"type":"string","enum":["issued","docs_presented","under_review","accepted","discrepant","settled"]},"description":"Filter by status"}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/bank-ops.LetterOfCredit"}},"nextCursor":{"type":"string","nullable":true},"total":{"type":"integer"}}}}}}},"description":"Letters of credit issued for a bank customer in Purple Bank Ops — applicant, beneficiary, amount, incoterm, expiry — each carrying a milestone chain (application → issuance → shipment → doc_presentation → payment) with bottleneck notes, and a status from issued through settled. Supports server-side querying with _filter/_search/_orderby/_select/_top/_skip/_count (OData $-params, renamed with a leading underscore for MCP), plus this tool's simple equality params (country, branch_id, status, …).","tags":["bank-ops"]},"post":{"summary":"Create letter of credit","parameters":[{"$ref":"#/components/parameters/bank-ops.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/bank-ops.LetterOfCredit"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/bank-ops.LetterOfCredit"}}}}},"tags":["bank-ops"]}},"/purple-suite/bank-ops/letters_of_credit/{id}":{"get":{"summary":"Get letter of credit","parameters":[{"$ref":"#/components/parameters/bank-ops.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/bank-ops.LetterOfCredit"}}}},"404":{"description":"Not found"}},"tags":["bank-ops"]},"patch":{"summary":"Update letter of credit","parameters":[{"$ref":"#/components/parameters/bank-ops.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/bank-ops.LetterOfCredit"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/bank-ops.LetterOfCredit"}}}},"404":{"description":"Not found"}},"tags":["bank-ops"]},"delete":{"summary":"Delete letter of credit","parameters":[{"$ref":"#/components/parameters/bank-ops.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"id":{"type":"string"}}}}}},"404":{"description":"Not found"}},"tags":["bank-ops"]}},"/purple-suite/bank-ops/letters_of_credit/{id}/advance-milestone":{"post":{"summary":"Advance a letter-of-credit milestone","description":"Updates a milestone (application | issuance | shipment | doc_presentation | payment), optionally recording a bottleneck_note, and optionally advances the LC status. A settled LC is terminal.","parameters":[{"$ref":"#/components/parameters/bank-ops.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"milestone":{"type":"string","enum":["application","issuance","shipment","doc_presentation","payment"]},"status":{"type":"string","example":"complete"},"bottleneck_note":{"type":"string"},"lc_status":{"type":"string","enum":["issued","docs_presented","under_review","accepted","discrepant","settled"]}},"required":["milestone"]}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/bank-ops.LetterOfCredit"}}}},"400":{"description":"Invalid milestone/status"},"404":{"description":"Not found"},"422":{"description":"LC already settled"}},"tags":["bank-ops"]}},"/purple-suite/bank-ops/bank_guarantees":{"get":{"summary":"List trade-finance bank guarantees","parameters":[{"$ref":"#/components/parameters/bank-ops.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Max items per page (default 50)"},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Pass the previous response's nextCursor to fetch the next page"},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."},{"in":"query","name":"country","required":false,"schema":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"description":"Filter to a single country (IN | AU | US)"},{"in":"query","name":"customer_id","required":false,"schema":{"type":"string"},"description":"Filter by customer id"},{"in":"query","name":"type","required":false,"schema":{"type":"string","enum":["performance","financial","bid_bond"]},"description":"Filter by type"},{"in":"query","name":"status","required":false,"schema":{"type":"string","enum":["active","invoked","expired","released"]},"description":"Filter by status"}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/bank-ops.BankGuarantee"}},"nextCursor":{"type":"string","nullable":true},"total":{"type":"integer"}}}}}}},"description":"Guarantees the bank has issued on behalf of a customer in Purple Bank Ops — performance, financial or bid_bond — with amount, expiry date and status (active | invoked | expired | released). Guarantees back a customer's obligation; letters of credit settle a trade shipment. Supports server-side querying with _filter/_search/_orderby/_select/_top/_skip/_count (OData $-params, renamed with a leading underscore for MCP), plus this tool's simple equality params (country, branch_id, status, …).","tags":["bank-ops"]},"post":{"summary":"Create bank guarantee","parameters":[{"$ref":"#/components/parameters/bank-ops.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/bank-ops.BankGuarantee"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/bank-ops.BankGuarantee"}}}}},"tags":["bank-ops"]}},"/purple-suite/bank-ops/bank_guarantees/{id}":{"get":{"summary":"Get bank guarantee","parameters":[{"$ref":"#/components/parameters/bank-ops.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/bank-ops.BankGuarantee"}}}},"404":{"description":"Not found"}},"tags":["bank-ops"]},"patch":{"summary":"Update bank guarantee","parameters":[{"$ref":"#/components/parameters/bank-ops.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/bank-ops.BankGuarantee"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/bank-ops.BankGuarantee"}}}},"404":{"description":"Not found"}},"tags":["bank-ops"]},"delete":{"summary":"Delete bank guarantee","parameters":[{"$ref":"#/components/parameters/bank-ops.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"id":{"type":"string"}}}}}},"404":{"description":"Not found"}},"tags":["bank-ops"]}},"/purple-suite/calendar/events":{"get":{"summary":"List Purple Calendar events","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"$ref":"#/components/parameters/calendar.Top"},{"$ref":"#/components/parameters/calendar.Skip"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Alias for $top — max items per page (default 50)"}],"responses":{"200":{"description":"Paginated list of events","content":{"application/json":{"schema":{"type":"object","properties":{"value":{"type":"array","items":{"$ref":"#/components/schemas/calendar.Event"}},"@odata.nextLink":{"type":"string","nullable":true,"description":"OData next-page link, absent on the last page"}}}}}}},"description":"Every meeting and appointment stored in Purple Calendar, across all users, as Microsoft Graph-shaped Event objects in a `value` array. This op only pages with _top/_skip — when the question is about a person or a date range, prefer the calendarView tools, which take startDateTime/endDateTime.","tags":["calendar"]},"post":{"summary":"Schedule a calendar event","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.Event"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.Event"}}}}},"description":"Creates a meeting in Purple Calendar — subject, start/end as DateTimeTimeZone, attendees, location or room, and optional recurrence (which makes it a series master). It does not check availability: use the free/busy schedule or findMeetingTimes tools first if the time isn't already agreed.","tags":["calendar"]}},"/purple-suite/calendar/events/{id}":{"get":{"summary":"Get event","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.Event"}}}},"404":{"description":"Not found"}},"tags":["calendar"]},"patch":{"summary":"Update a calendar event","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.Event"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.Event"}}}},"404":{"description":"Not found"}},"description":"Partially updates an existing Purple Calendar event — reschedule its times, change the subject/body, or amend the attendee list. To call a meeting off, prefer the cancel tool, which keeps the record and marks it isCancelled.","tags":["calendar"]},"delete":{"summary":"Delete a calendar event","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No content"},"404":{"description":"Not found"}},"description":"Permanently removes an event from Purple Calendar. Destructive and irreversible, and it leaves attendees no cancellation record — use the cancel tool instead whenever a meeting is simply called off.","tags":["calendar"]}},"/purple-suite/calendar/users/{email}/events/{id}/accept":{"post":{"summary":"Accept event invitation","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"email","required":true,"schema":{"type":"string","format":"email"},"description":"User email — or the literal \"me\" to act as the current impersonated user (the X-PS-Impersonate-User header, or the instance's configured default MCP user)."},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"comment":{"type":"string","example":"Looking forward to it!"},"sendResponse":{"type":"boolean","example":true}}}}}},"responses":{"202":{"description":"Accepted"},"404":{"description":"Not found"}},"tags":["calendar"]}},"/purple-suite/calendar/users/{email}/events/{id}/decline":{"post":{"summary":"Decline event invitation","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"email","required":true,"schema":{"type":"string","format":"email"},"description":"User email — or the literal \"me\" to act as the current impersonated user (the X-PS-Impersonate-User header, or the instance's configured default MCP user)."},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"comment":{"type":"string","example":"Conflict — will catch the recording."},"sendResponse":{"type":"boolean","example":true},"proposedNewTime":{"type":"object","nullable":true}}}}}},"responses":{"202":{"description":"Accepted"},"404":{"description":"Not found"}},"tags":["calendar"]}},"/purple-suite/calendar/users/{email}/events/{id}/tentativelyAccept":{"post":{"summary":"Tentatively accept event invitation","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"email","required":true,"schema":{"type":"string","format":"email"},"description":"User email — or the literal \"me\" to act as the current impersonated user (the X-PS-Impersonate-User header, or the instance's configured default MCP user)."},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"comment":{"type":"string","example":"Will try to join."},"sendResponse":{"type":"boolean","example":true},"proposedNewTime":{"type":"object","nullable":true}}}}}},"responses":{"202":{"description":"Accepted"},"404":{"description":"Not found"}},"tags":["calendar"]}},"/purple-suite/calendar/events/{id}/cancel":{"post":{"summary":"Cancel event (bare path alias)","description":"Cancels the event (sets isCancelled: true, showAs: free). Does not delete the event. Cancelling a seriesMaster cancels the whole series; cancelling a generated occurrence (id in the form seriesMasterId_yyyymmdd) cancels just that date by materializing an exception. Also available at /users/{email}/events/{id}/cancel.","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"comment":{"type":"string","description":"Cancellation message","example":"Meeting is no longer needed."}}}}}},"responses":{"202":{"description":"Accepted"},"404":{"description":"Not found"}},"tags":["calendar"]}},"/purple-suite/calendar/users/{email}/events/{id}/cancel":{"post":{"summary":"Cancel event (user-scoped path)","description":"Cancels the event (sets isCancelled: true, showAs: free). Does not delete the event. Cancelling a seriesMaster cancels the whole series; cancelling a generated occurrence (id in the form seriesMasterId_yyyymmdd) cancels just that date by materializing an exception.","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"email","required":true,"schema":{"type":"string","format":"email"},"description":"User email — or the literal \"me\" to act as the current impersonated user (the X-PS-Impersonate-User header, or the instance's configured default MCP user)."},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"comment":{"type":"string","description":"Cancellation message","example":"Meeting is no longer needed."}}}}}},"responses":{"202":{"description":"Accepted"},"404":{"description":"Not found"}},"tags":["calendar"]}},"/purple-suite/calendar/events/{id}/instances":{"get":{"summary":"List occurrences of an event series (bare path alias)","description":"Expands a seriesMaster into its occurrence instances within the time window (stored exceptions included). For a non-recurring event, returns the event itself when it intersects the window. Also available at /users/{email}/events/{id}/instances.","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}},{"in":"query","name":"startDateTime","required":true,"schema":{"type":"string","format":"date-time"},"example":"2026-06-01T00:00:00Z"},{"in":"query","name":"endDateTime","required":true,"schema":{"type":"string","format":"date-time"},"example":"2026-06-30T23:59:59Z"}],"responses":{"200":{"description":"Occurrences in range","content":{"application/json":{"schema":{"type":"object","properties":{"value":{"type":"array","items":{"$ref":"#/components/schemas/calendar.Event"}}}}}}},"400":{"description":"startDateTime and endDateTime are required"},"404":{"description":"Not found"}},"tags":["calendar"]}},"/purple-suite/calendar/users/{email}/events/{id}/instances":{"get":{"summary":"List occurrences of an event series (user-scoped path)","description":"Expands a seriesMaster into its occurrence instances within the time window (stored exceptions included). For a non-recurring event, returns the event itself when it intersects the window.","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"email","required":true,"schema":{"type":"string","format":"email"},"description":"User email — or the literal \"me\" to act as the current impersonated user (the X-PS-Impersonate-User header, or the instance's configured default MCP user)."},{"in":"path","name":"id","required":true,"schema":{"type":"string"}},{"in":"query","name":"startDateTime","required":true,"schema":{"type":"string","format":"date-time"},"example":"2026-06-01T00:00:00Z"},{"in":"query","name":"endDateTime","required":true,"schema":{"type":"string","format":"date-time"},"example":"2026-06-30T23:59:59Z"}],"responses":{"200":{"description":"Occurrences in range","content":{"application/json":{"schema":{"type":"object","properties":{"value":{"type":"array","items":{"$ref":"#/components/schemas/calendar.Event"}}}}}}},"400":{"description":"startDateTime and endDateTime are required"},"404":{"description":"Not found"}},"tags":["calendar"]}},"/purple-suite/calendar/findMeetingTimes":{"post":{"summary":"Find meeting time suggestions (bare path)","description":"Suggests meeting times when all attendees are available, based on their calendar events. Also available at /users/{email}/findMeetingTimes.","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"header","name":"Prefer","schema":{"type":"string"},"description":"Optional response timezone override, e.g. `outlook.timezone=\"Pacific Standard Time\"`","example":"outlook.timezone=\"Pacific Standard Time\""}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"attendees":{"type":"array","items":{"type":"object","properties":{"emailAddress":{"$ref":"#/components/schemas/calendar.EmailAddress"},"type":{"type":"string","enum":["required","optional","resource"]}}},"example":[{"emailAddress":{"address":"bob.martinez@acme.com","name":"Bob Martinez"},"type":"required"}]},"timeConstraint":{"type":"object","properties":{"activityDomain":{"type":"string","enum":["work","personal","unrestricted"],"example":"work"},"timeSlots":{"type":"array","items":{"type":"object","properties":{"start":{"$ref":"#/components/schemas/calendar.DateTimeTimeZone"},"end":{"$ref":"#/components/schemas/calendar.DateTimeTimeZone"}}},"example":[{"start":{"dateTime":"2026-06-02T08:00:00.0000000","timeZone":"Pacific Standard Time"},"end":{"dateTime":"2026-06-02T17:00:00.0000000","timeZone":"Pacific Standard Time"}}]}}},"meetingDuration":{"type":"string","description":"ISO 8601 duration, e.g. PT1H, PT30M","default":"PT30M","example":"PT30M"},"maxCandidates":{"type":"integer","default":5,"example":5},"minimumAttendeePercentage":{"type":"number","default":50,"example":50},"isOrganizerOptional":{"type":"boolean","default":false,"example":false},"returnSuggestionReasons":{"type":"boolean","default":false,"example":false}}}}}},"responses":{"200":{"description":"Meeting time suggestions","content":{"application/json":{"schema":{"type":"object","properties":{"emptySuggestionsReason":{"type":"string","example":""},"meetingTimeSuggestions":{"type":"array","items":{"type":"object","properties":{"confidence":{"type":"number","example":100},"order":{"type":"integer","example":1},"organizerAvailability":{"type":"string","example":"free"},"suggestionReason":{"type":"string"},"attendeeAvailability":{"type":"array","items":{"type":"object","properties":{"attendee":{"type":"object"},"availability":{"type":"string"}}}},"locations":{"type":"array","items":{"type":"object"}},"meetingTimeSlot":{"type":"object","properties":{"start":{"$ref":"#/components/schemas/calendar.DateTimeTimeZone"},"end":{"$ref":"#/components/schemas/calendar.DateTimeTimeZone"}}}}}}}}}}}},"tags":["calendar"]}},"/purple-suite/calendar/users/{email}/findMeetingTimes":{"post":{"summary":"Find meeting time suggestions (user-scoped path)","description":"Organizer-scoped variant. Uses the organizer's timezone from their mailboxSettings for activityDomain filtering.","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"email","required":true,"schema":{"type":"string","format":"email"},"description":"User email — or the literal \"me\" to act as the current impersonated user (the X-PS-Impersonate-User header, or the instance's configured default MCP user)."},{"in":"header","name":"Prefer","schema":{"type":"string"},"description":"Optional response timezone override, e.g. `outlook.timezone=\"Pacific Standard Time\"`","example":"outlook.timezone=\"Pacific Standard Time\""}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"attendees":{"type":"array","items":{"type":"object","properties":{"emailAddress":{"$ref":"#/components/schemas/calendar.EmailAddress"},"type":{"type":"string","enum":["required","optional","resource"]}}},"example":[{"emailAddress":{"address":"bob.martinez@acme.com","name":"Bob Martinez"},"type":"required"}]},"timeConstraint":{"type":"object","properties":{"activityDomain":{"type":"string","enum":["work","personal","unrestricted"],"example":"work"},"timeSlots":{"type":"array","items":{"type":"object","properties":{"start":{"$ref":"#/components/schemas/calendar.DateTimeTimeZone"},"end":{"$ref":"#/components/schemas/calendar.DateTimeTimeZone"}}},"example":[{"start":{"dateTime":"2026-06-02T08:00:00.0000000","timeZone":"Pacific Standard Time"},"end":{"dateTime":"2026-06-02T17:00:00.0000000","timeZone":"Pacific Standard Time"}}]}}},"meetingDuration":{"type":"string","description":"ISO 8601 duration, e.g. PT1H, PT30M","default":"PT30M","example":"PT30M"},"maxCandidates":{"type":"integer","default":5,"example":5},"minimumAttendeePercentage":{"type":"number","default":50,"example":50},"isOrganizerOptional":{"type":"boolean","default":false,"example":false},"returnSuggestionReasons":{"type":"boolean","default":false,"example":false}}}}}},"responses":{"200":{"description":"Meeting time suggestions","content":{"application/json":{"schema":{"type":"object","properties":{"emptySuggestionsReason":{"type":"string","example":""},"meetingTimeSuggestions":{"type":"array","items":{"type":"object","properties":{"confidence":{"type":"number","example":100},"order":{"type":"integer","example":1},"organizerAvailability":{"type":"string","example":"free"},"suggestionReason":{"type":"string"},"attendeeAvailability":{"type":"array","items":{"type":"object","properties":{"attendee":{"type":"object"},"availability":{"type":"string"}}}},"locations":{"type":"array","items":{"type":"object"}},"meetingTimeSlot":{"type":"object","properties":{"start":{"$ref":"#/components/schemas/calendar.DateTimeTimeZone"},"end":{"$ref":"#/components/schemas/calendar.DateTimeTimeZone"}}}}}}}}}}}},"tags":["calendar"]}},"/purple-suite/calendar/calendarView":{"get":{"summary":"Get calendar view — all events in time range","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"query","name":"startDateTime","required":true,"schema":{"type":"string","format":"date-time"},"example":"2026-06-02T00:00:00Z"},{"in":"query","name":"endDateTime","required":true,"schema":{"type":"string","format":"date-time"},"example":"2026-06-06T23:59:59Z"},{"$ref":"#/components/parameters/calendar.Top"},{"$ref":"#/components/parameters/calendar.Skip"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Alias for $top — max items per page (default 50)"}],"responses":{"200":{"description":"Events in range","content":{"application/json":{"schema":{"type":"object","properties":{"value":{"type":"array","items":{"$ref":"#/components/schemas/calendar.Event"}},"@odata.nextLink":{"type":"string","nullable":true,"description":"OData next-page link, absent on the last page"}}}}}},"400":{"description":"startDateTime and endDateTime are required"}},"tags":["calendar"]}},"/purple-suite/calendar/users/{email}/calendarView":{"get":{"summary":"Get calendar view for a specific user (events in time range)","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"email","required":true,"schema":{"type":"string","format":"email"},"description":"User email — or the literal \"me\" to act as the current impersonated user (the X-PS-Impersonate-User header, or the instance's configured default MCP user)."},{"in":"query","name":"startDateTime","required":true,"schema":{"type":"string","format":"date-time"},"example":"2026-06-02T00:00:00Z"},{"in":"query","name":"endDateTime","required":true,"schema":{"type":"string","format":"date-time"},"example":"2026-06-06T23:59:59Z"}],"responses":{"200":{"description":"User events in range","content":{"application/json":{"schema":{"type":"object","properties":{"value":{"type":"array","items":{"$ref":"#/components/schemas/calendar.Event"}}}}}}},"400":{"description":"startDateTime and endDateTime are required"}},"tags":["calendar"]}},"/purple-suite/calendar/users/{email}/events":{"get":{"summary":"List all events for a specific user","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"email","required":true,"schema":{"type":"string","format":"email"},"description":"User email — or the literal \"me\" to act as the current impersonated user (the X-PS-Impersonate-User header, or the instance's configured default MCP user)."},{"$ref":"#/components/parameters/calendar.Top"},{"$ref":"#/components/parameters/calendar.Skip"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Alias for $top — max items per page (default 50)"}],"responses":{"200":{"description":"User events","content":{"application/json":{"schema":{"type":"object","properties":{"value":{"type":"array","items":{"$ref":"#/components/schemas/calendar.Event"}},"@odata.nextLink":{"type":"string","nullable":true,"description":"OData next-page link, absent on the last page"}}}}}}},"tags":["calendar"]},"post":{"x-mcp":false,"summary":"Create event (user-scoped path)","description":"User-scoped twin of POST /events. Defaults the organizer to the resolved user when the body doesn't set one.","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"email","required":true,"schema":{"type":"string","format":"email"},"description":"User email — or the literal \"me\" to act as the current impersonated user (the X-PS-Impersonate-User header, or the instance's configured default MCP user)."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.Event"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.Event"}}}}},"tags":["calendar"]}},"/purple-suite/calendar/users/{email}/events/{id}":{"get":{"x-mcp":false,"summary":"Get event (user-scoped path)","description":"User-scoped twin of GET /events/{id}.","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"email","required":true,"schema":{"type":"string","format":"email"},"description":"User email — or the literal \"me\" to act as the current impersonated user (the X-PS-Impersonate-User header, or the instance's configured default MCP user)."},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.Event"}}}},"404":{"description":"Not found"}},"tags":["calendar"]},"patch":{"x-mcp":false,"summary":"Update event (user-scoped path)","description":"User-scoped twin of PATCH /events/{id}.","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"email","required":true,"schema":{"type":"string","format":"email"},"description":"User email — or the literal \"me\" to act as the current impersonated user (the X-PS-Impersonate-User header, or the instance's configured default MCP user)."},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.Event"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.Event"}}}},"404":{"description":"Not found"}},"tags":["calendar"]},"delete":{"x-mcp":false,"summary":"Delete event (user-scoped path)","description":"User-scoped twin of DELETE /events/{id}.","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"email","required":true,"schema":{"type":"string","format":"email"},"description":"User email — or the literal \"me\" to act as the current impersonated user (the X-PS-Impersonate-User header, or the instance's configured default MCP user)."},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No content"},"404":{"description":"Not found"}},"tags":["calendar"]}},"/purple-suite/calendar/users/{email}/calendars":{"get":{"x-mcp":false,"summary":"List all calendars (user-scoped alias — returns all instance calendars)","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"email","required":true,"schema":{"type":"string","format":"email"},"description":"User email — or the literal \"me\" to act as the current impersonated user (the X-PS-Impersonate-User header, or the instance's configured default MCP user)."},{"$ref":"#/components/parameters/calendar.Top"},{"$ref":"#/components/parameters/calendar.Skip"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Alias for $top — max items per page (default 50)"}],"responses":{"200":{"description":"Collection of calendars","content":{"application/json":{"schema":{"type":"object","properties":{"value":{"type":"array","items":{"$ref":"#/components/schemas/calendar.Calendar"}},"@odata.nextLink":{"type":"string","nullable":true,"description":"OData next-page link, absent on the last page"}}}}}}},"tags":["calendar"]},"post":{"x-mcp":false,"summary":"Create calendar (user-scoped path)","description":"User-scoped twin of POST /calendars. Defaults the owner to the resolved user when the body doesn't set one.","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"email","required":true,"schema":{"type":"string","format":"email"},"description":"User email — or the literal \"me\" to act as the current impersonated user (the X-PS-Impersonate-User header, or the instance's configured default MCP user)."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.Calendar"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.Calendar"}}}}},"tags":["calendar"]}},"/purple-suite/calendar/users/{email}/calendars/{id}":{"get":{"x-mcp":false,"summary":"Get calendar (user-scoped path)","description":"User-scoped twin of GET /calendars/{id}.","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"email","required":true,"schema":{"type":"string","format":"email"},"description":"User email — or the literal \"me\" to act as the current impersonated user (the X-PS-Impersonate-User header, or the instance's configured default MCP user)."},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.Calendar"}}}},"404":{"description":"Not found"}},"tags":["calendar"]},"patch":{"x-mcp":false,"summary":"Update calendar (user-scoped path)","description":"User-scoped twin of PATCH /calendars/{id}.","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"email","required":true,"schema":{"type":"string","format":"email"},"description":"User email — or the literal \"me\" to act as the current impersonated user (the X-PS-Impersonate-User header, or the instance's configured default MCP user)."},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.Calendar"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.Calendar"}}}},"404":{"description":"Not found"}},"tags":["calendar"]},"delete":{"x-mcp":false,"summary":"Delete calendar (user-scoped path)","description":"User-scoped twin of DELETE /calendars/{id}.","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"email","required":true,"schema":{"type":"string","format":"email"},"description":"User email — or the literal \"me\" to act as the current impersonated user (the X-PS-Impersonate-User header, or the instance's configured default MCP user)."},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No content"},"404":{"description":"Not found"}},"tags":["calendar"]}},"/purple-suite/calendar/users/{email}/calendars/{id}/events":{"get":{"summary":"List events for a specific calendar (user-scoped path)","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"email","required":true,"schema":{"type":"string","format":"email"},"description":"User email — or the literal \"me\" to act as the current impersonated user (the X-PS-Impersonate-User header, or the instance's configured default MCP user)."},{"in":"path","name":"id","required":true,"schema":{"type":"string"},"description":"Calendar id"},{"in":"query","name":"startDateTime","schema":{"type":"string","format":"date-time"},"example":"2026-06-02T00:00:00Z"},{"in":"query","name":"endDateTime","schema":{"type":"string","format":"date-time"},"example":"2026-06-06T23:59:59Z"}],"responses":{"200":{"description":"Events for the calendar","content":{"application/json":{"schema":{"type":"object","properties":{"value":{"type":"array","items":{"$ref":"#/components/schemas/calendar.Event"}}}}}}},"404":{"description":"Calendar not found"}},"tags":["calendar"]},"post":{"summary":"Create event in a specific calendar (user-scoped path)","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"email","required":true,"schema":{"type":"string","format":"email"},"description":"User email — or the literal \"me\" to act as the current impersonated user (the X-PS-Impersonate-User header, or the instance's configured default MCP user)."},{"in":"path","name":"id","required":true,"schema":{"type":"string"},"description":"Calendar id"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.Event"}}}},"responses":{"201":{"description":"Created event","content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.Event"}}}},"404":{"description":"Calendar not found"}},"tags":["calendar"]}},"/purple-suite/calendar/calendars":{"get":{"summary":"List Purple Calendar calendars","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"$ref":"#/components/parameters/calendar.Top"},{"$ref":"#/components/parameters/calendar.Skip"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Alias for $top — max items per page (default 50)"}],"responses":{"200":{"description":"Paginated list of calendars","content":{"application/json":{"schema":{"type":"object","properties":{"value":{"type":"array","items":{"$ref":"#/components/schemas/calendar.Calendar"}},"@odata.nextLink":{"type":"string","nullable":true,"description":"OData next-page link, absent on the last page"}}}}}}},"description":"The calendar containers in this instance — each person's default calendar plus any shared or resource calendars, with owner, colour and sharing flags. These are the buckets events live in, not the events themselves; use it to resolve a calendar id before listing or creating events inside one.","tags":["calendar"]},"post":{"summary":"Create calendar","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.Calendar"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.Calendar"}}}}},"tags":["calendar"]}},"/purple-suite/calendar/calendars/{id}":{"get":{"summary":"Get calendar","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.Calendar"}}}},"404":{"description":"Not found"}},"tags":["calendar"]},"patch":{"summary":"Update calendar","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.Calendar"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.Calendar"}}}},"404":{"description":"Not found"}},"tags":["calendar"]},"delete":{"summary":"Delete calendar","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No content"},"404":{"description":"Not found"}},"tags":["calendar"]}},"/purple-suite/calendar/calendars/{id}/events":{"get":{"summary":"List events for a specific calendar","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}},{"in":"query","name":"startDateTime","schema":{"type":"string","format":"date-time"},"example":"2026-06-02T00:00:00Z"},{"in":"query","name":"endDateTime","schema":{"type":"string","format":"date-time"},"example":"2026-06-06T23:59:59Z"}],"responses":{"200":{"description":"Events for the calendar","content":{"application/json":{"schema":{"type":"object","properties":{"value":{"type":"array","items":{"$ref":"#/components/schemas/calendar.Event"}}}}}}},"404":{"description":"Calendar not found"}},"tags":["calendar"]},"post":{"summary":"Create event in a specific calendar","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.Event"}}}},"responses":{"201":{"description":"Created event","content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.Event"}}}},"404":{"description":"Calendar not found"}},"tags":["calendar"]}},"/purple-suite/calendar/calendar/getSchedule":{"post":{"summary":"Get free/busy schedule for users (bare path alias)","description":"Returns availability view and schedule items for each requested SMTP address. Also available at /users/{email}/calendar/getSchedule.","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"schedules":{"type":"array","items":{"type":"string","format":"email"},"description":"SMTP addresses to query","example":["alice.chen@acme.com","bob.martinez@acme.com"]},"startTime":{"allOf":[{"$ref":"#/components/schemas/calendar.DateTimeTimeZone"}],"example":{"dateTime":"2026-06-02T08:00:00.0000000","timeZone":"Pacific Standard Time"}},"endTime":{"allOf":[{"$ref":"#/components/schemas/calendar.DateTimeTimeZone"}],"example":{"dateTime":"2026-06-06T18:00:00.0000000","timeZone":"Pacific Standard Time"}},"availabilityViewInterval":{"type":"integer","default":30,"description":"Minutes per availability slot","example":30}},"required":["schedules","startTime","endTime"]}}}},"responses":{"200":{"description":"Schedule information","content":{"application/json":{"schema":{"type":"object","properties":{"value":{"type":"array","items":{"$ref":"#/components/schemas/calendar.ScheduleInformation"}}}}}}},"400":{"description":"schedules, startTime, and endTime are required"}},"tags":["calendar"]}},"/purple-suite/calendar/users/{email}/calendar/getSchedule":{"post":{"summary":"Get free/busy schedule for users (user-scoped path)","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"email","required":true,"schema":{"type":"string","format":"email"},"description":"User email — or the literal \"me\" to act as the current impersonated user (the X-PS-Impersonate-User header, or the instance's configured default MCP user)."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"schedules":{"type":"array","items":{"type":"string","format":"email"},"description":"SMTP addresses to query","example":["alice.chen@acme.com","bob.martinez@acme.com"]},"startTime":{"allOf":[{"$ref":"#/components/schemas/calendar.DateTimeTimeZone"}],"example":{"dateTime":"2026-06-02T08:00:00.0000000","timeZone":"Pacific Standard Time"}},"endTime":{"allOf":[{"$ref":"#/components/schemas/calendar.DateTimeTimeZone"}],"example":{"dateTime":"2026-06-06T18:00:00.0000000","timeZone":"Pacific Standard Time"}},"availabilityViewInterval":{"type":"integer","default":30,"description":"Minutes per availability slot","example":30}},"required":["schedules","startTime","endTime"]}}}},"responses":{"200":{"description":"Schedule information","content":{"application/json":{"schema":{"type":"object","properties":{"value":{"type":"array","items":{"$ref":"#/components/schemas/calendar.ScheduleInformation"}}}}}}}},"tags":["calendar"]}},"/purple-suite/calendar/places":{"get":{"summary":"List bookable meeting rooms","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"$ref":"#/components/parameters/calendar.Top"},{"$ref":"#/components/parameters/calendar.Skip"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Alias for $top — max items per page (default 50)"}],"responses":{"200":{"description":"Paginated list of rooms","content":{"application/json":{"schema":{"type":"object","properties":{"value":{"type":"array","items":{"$ref":"#/components/schemas/calendar.Room"}},"@odata.nextLink":{"type":"string","nullable":true,"description":"OData next-page link, absent on the last page"}}}}}}},"description":"Physical meeting rooms in Purple Calendar — display name, room mailbox address, capacity, building/floor, accessibility and equipment tags (whiteboard, tv, video_conferencing). Use it to pick a room to attach as an event location or resource attendee; it is not an office/site directory and holds no people. Pages with _top/_skip.","tags":["calendar"]},"post":{"summary":"Create room","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.Room"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.Room"}}}}},"tags":["calendar"]}},"/purple-suite/calendar/places/{id}":{"get":{"summary":"Get room","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.Room"}}}},"404":{"description":"Not found"}},"tags":["calendar"]},"patch":{"summary":"Update room","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.Room"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.Room"}}}},"404":{"description":"Not found"}},"tags":["calendar"]},"delete":{"summary":"Delete room","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No content"},"404":{"description":"Not found"}},"tags":["calendar"]}},"/purple-suite/calendar/users/{email}/mailboxSettings":{"get":{"summary":"Get user mailbox settings","description":"Returns a Graph-compatible MailboxSettings projection of the stored UserSettings record. Returns sensible defaults for unknown users.","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"email","required":true,"schema":{"type":"string","format":"email"},"description":"User email — or the literal \"me\" to act as the current impersonated user (the X-PS-Impersonate-User header, or the instance's configured default MCP user)."}],"responses":{"200":{"description":"Mailbox settings","content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.MailboxSettings"}}}}},"tags":["calendar"]},"patch":{"summary":"Update user mailbox settings","description":"Merges the provided fields into the user's settings. Creates a record if none exists. Validates timeZone and workingHours.","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"email","required":true,"schema":{"type":"string","format":"email"},"description":"User email — or the literal \"me\" to act as the current impersonated user (the X-PS-Impersonate-User header, or the instance's configured default MCP user)."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.MailboxSettings"}}}},"responses":{"200":{"description":"Updated mailbox settings","content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.MailboxSettings"}}}},"422":{"description":"Invalid timeZone or workingHours"}},"tags":["calendar"]}},"/purple-suite/calendar/distribution_lists":{"get":{"summary":"List email distribution lists","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Max items per page (default 50)"},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Pass the previous response's nextCursor to fetch the next page"},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list of distribution lists","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/calendar.DistributionList"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page"},"total":{"type":"integer","description":"Total documents in the collection"}}}}}}},"description":"Named email groups in Purple Calendar (e.g. engineering-all@acme.com) with their member addresses and memberCount — used to invite a whole group to a meeting. These are mailing groups, not chat channels (see the chat app) or HR departments (see the hris app). Supports server-side querying with _filter/_search/_orderby/_select/_top/_skip/_count (OData $-params, renamed with a leading underscore for MCP).","tags":["calendar"]},"post":{"summary":"Create distribution list","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.DistributionList"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.DistributionList"}}}}},"tags":["calendar"]}},"/purple-suite/calendar/distribution_lists/{id}":{"get":{"summary":"Get distribution list","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.DistributionList"}}}},"404":{"description":"Not found"}},"tags":["calendar"]},"patch":{"summary":"Update distribution list","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.DistributionList"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.DistributionList"}}}},"404":{"description":"Not found"}},"tags":["calendar"]},"delete":{"summary":"Delete distribution list","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["calendar"]}},"/purple-suite/calendar/distribution_lists/{id}/members":{"post":{"summary":"Add members to a distribution list","description":"Appends new email addresses to the members array (duplicates are silently skipped). Updates memberCount.","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"emails":{"type":"array","items":{"type":"string","format":"email"},"example":["carol.johnson@acme.com","dave.williams@acme.com"]}},"required":["emails"]}}}},"responses":{"200":{"description":"Updated distribution list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.DistributionList"}}}},"404":{"description":"Not found"},"422":{"description":"emails array is required and must not be empty"}},"tags":["calendar"]}},"/purple-suite/calendar/distribution_lists/{id}/members/{email}":{"delete":{"summary":"Remove a member from a distribution list","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}},{"in":"path","name":"email","required":true,"schema":{"type":"string","format":"email"},"description":"Email address of the member to remove"}],"responses":{"200":{"description":"Updated distribution list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.DistributionList"}}}},"404":{"description":"Not found (list or member)"}},"tags":["calendar"]}},"/purple-suite/calendar/user_settings":{"get":{"summary":"List calendar user (mailbox) settings","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Max items per page (default 50)"},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Pass the previous response's nextCursor to fetch the next page"},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list of user settings","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/calendar.UserSettings"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page"},"total":{"type":"integer","description":"Total documents in the collection"}}}}}}},"description":"Raw UserSettings records — one per mailbox in Purple Calendar — holding each person's email, display name, timezone, working hours, date/time format, locale and auto-reply setting. This is the storage-level view and the way to enumerate who has a mailbox; for one known user prefer the Graph-shaped mailboxSettings tool. Supports server-side querying with _filter/_search/_orderby/_select/_top/_skip/_count (OData $-params, renamed with a leading underscore for MCP).","tags":["calendar"]},"post":{"summary":"Create user settings record","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.UserSettings"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.UserSettings"}}}}},"tags":["calendar"]}},"/purple-suite/calendar/user_settings/{id}":{"get":{"summary":"Get user settings record","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.UserSettings"}}}},"404":{"description":"Not found"}},"tags":["calendar"]},"patch":{"summary":"Update a calendar user settings record","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.UserSettings"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.UserSettings"}}}},"404":{"description":"Not found"}},"description":"Partially updates one raw UserSettings record by its document id (timezone, working hours, locale, auto-replies). Use the mailboxSettings update tool instead when you have the person's email address rather than the record id.","tags":["calendar"]},"delete":{"summary":"Delete user settings record","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["calendar"]}},"/purple-suite/calendar/me/events/{id}/accept":{"post":{"summary":"Accept event invitation (/me alias)","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"comment":{"type":"string","example":"Looking forward to it!"},"sendResponse":{"type":"boolean","example":true}}}}}},"responses":{"202":{"description":"Accepted"},"404":{"description":"Not found"}},"x-mcp":false,"description":"Alias of /users/{email}/events/{id}/accept: acts as the impersonated user (X-PS-Impersonate-User header, or the instance's default MCP user).","tags":["calendar"]}},"/purple-suite/calendar/me/events/{id}/decline":{"post":{"summary":"Decline event invitation (/me alias)","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"comment":{"type":"string","example":"Conflict — will catch the recording."},"sendResponse":{"type":"boolean","example":true},"proposedNewTime":{"type":"object","nullable":true}}}}}},"responses":{"202":{"description":"Accepted"},"404":{"description":"Not found"}},"x-mcp":false,"description":"Alias of /users/{email}/events/{id}/decline: acts as the impersonated user (X-PS-Impersonate-User header, or the instance's default MCP user).","tags":["calendar"]}},"/purple-suite/calendar/me/events/{id}/tentativelyAccept":{"post":{"summary":"Tentatively accept event invitation (/me alias)","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"comment":{"type":"string","example":"Will try to join."},"sendResponse":{"type":"boolean","example":true},"proposedNewTime":{"type":"object","nullable":true}}}}}},"responses":{"202":{"description":"Accepted"},"404":{"description":"Not found"}},"x-mcp":false,"description":"Alias of /users/{email}/events/{id}/tentativelyAccept: acts as the impersonated user (X-PS-Impersonate-User header, or the instance's default MCP user).","tags":["calendar"]}},"/purple-suite/calendar/me/events/{id}/cancel":{"post":{"summary":"Cancel event (user-scoped path) (/me alias)","description":"Cancels the event (sets isCancelled: true, showAs: free). Does not delete the event. Cancelling a seriesMaster cancels the whole series; cancelling a generated occurrence (id in the form seriesMasterId_yyyymmdd) cancels just that date by materializing an exception. Alias of /users/{email}/events/{id}/cancel: acts as the impersonated user (X-PS-Impersonate-User header, or the instance's default MCP user).","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"comment":{"type":"string","description":"Cancellation message","example":"Meeting is no longer needed."}}}}}},"responses":{"202":{"description":"Accepted"},"404":{"description":"Not found"}},"x-mcp":false,"tags":["calendar"]}},"/purple-suite/calendar/me/events/{id}/instances":{"get":{"summary":"List occurrences of an event series (user-scoped path) (/me alias)","description":"Expands a seriesMaster into its occurrence instances within the time window (stored exceptions included). For a non-recurring event, returns the event itself when it intersects the window. Alias of /users/{email}/events/{id}/instances: acts as the impersonated user (X-PS-Impersonate-User header, or the instance's default MCP user).","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}},{"in":"query","name":"startDateTime","required":true,"schema":{"type":"string","format":"date-time"},"example":"2026-06-01T00:00:00Z"},{"in":"query","name":"endDateTime","required":true,"schema":{"type":"string","format":"date-time"},"example":"2026-06-30T23:59:59Z"}],"responses":{"200":{"description":"Occurrences in range","content":{"application/json":{"schema":{"type":"object","properties":{"value":{"type":"array","items":{"$ref":"#/components/schemas/calendar.Event"}}}}}}},"400":{"description":"startDateTime and endDateTime are required"},"404":{"description":"Not found"}},"x-mcp":false,"tags":["calendar"]}},"/purple-suite/calendar/me/findMeetingTimes":{"post":{"summary":"Find meeting time suggestions (user-scoped path) (/me alias)","description":"Organizer-scoped variant. Uses the organizer's timezone from their mailboxSettings for activityDomain filtering. Alias of /users/{email}/findMeetingTimes: acts as the impersonated user (X-PS-Impersonate-User header, or the instance's default MCP user).","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"header","name":"Prefer","schema":{"type":"string"},"description":"Optional response timezone override, e.g. `outlook.timezone=\"Pacific Standard Time\"`","example":"outlook.timezone=\"Pacific Standard Time\""}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"attendees":{"type":"array","items":{"type":"object","properties":{"emailAddress":{"$ref":"#/components/schemas/calendar.EmailAddress"},"type":{"type":"string","enum":["required","optional","resource"]}}},"example":[{"emailAddress":{"address":"bob.martinez@acme.com","name":"Bob Martinez"},"type":"required"}]},"timeConstraint":{"type":"object","properties":{"activityDomain":{"type":"string","enum":["work","personal","unrestricted"],"example":"work"},"timeSlots":{"type":"array","items":{"type":"object","properties":{"start":{"$ref":"#/components/schemas/calendar.DateTimeTimeZone"},"end":{"$ref":"#/components/schemas/calendar.DateTimeTimeZone"}}},"example":[{"start":{"dateTime":"2026-06-02T08:00:00.0000000","timeZone":"Pacific Standard Time"},"end":{"dateTime":"2026-06-02T17:00:00.0000000","timeZone":"Pacific Standard Time"}}]}}},"meetingDuration":{"type":"string","description":"ISO 8601 duration, e.g. PT1H, PT30M","default":"PT30M","example":"PT30M"},"maxCandidates":{"type":"integer","default":5,"example":5},"minimumAttendeePercentage":{"type":"number","default":50,"example":50},"isOrganizerOptional":{"type":"boolean","default":false,"example":false},"returnSuggestionReasons":{"type":"boolean","default":false,"example":false}}}}}},"responses":{"200":{"description":"Meeting time suggestions","content":{"application/json":{"schema":{"type":"object","properties":{"emptySuggestionsReason":{"type":"string","example":""},"meetingTimeSuggestions":{"type":"array","items":{"type":"object","properties":{"confidence":{"type":"number","example":100},"order":{"type":"integer","example":1},"organizerAvailability":{"type":"string","example":"free"},"suggestionReason":{"type":"string"},"attendeeAvailability":{"type":"array","items":{"type":"object","properties":{"attendee":{"type":"object"},"availability":{"type":"string"}}}},"locations":{"type":"array","items":{"type":"object"}},"meetingTimeSlot":{"type":"object","properties":{"start":{"$ref":"#/components/schemas/calendar.DateTimeTimeZone"},"end":{"$ref":"#/components/schemas/calendar.DateTimeTimeZone"}}}}}}}}}}}},"x-mcp":false,"tags":["calendar"]}},"/purple-suite/calendar/me/calendarView":{"get":{"summary":"Get calendar view for a specific user (events in time range) (/me alias)","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"query","name":"startDateTime","required":true,"schema":{"type":"string","format":"date-time"},"example":"2026-06-02T00:00:00Z"},{"in":"query","name":"endDateTime","required":true,"schema":{"type":"string","format":"date-time"},"example":"2026-06-06T23:59:59Z"}],"responses":{"200":{"description":"User events in range","content":{"application/json":{"schema":{"type":"object","properties":{"value":{"type":"array","items":{"$ref":"#/components/schemas/calendar.Event"}}}}}}},"400":{"description":"startDateTime and endDateTime are required"}},"x-mcp":false,"description":"Alias of /users/{email}/calendarView: acts as the impersonated user (X-PS-Impersonate-User header, or the instance's default MCP user).","tags":["calendar"]}},"/purple-suite/calendar/me/events":{"get":{"summary":"List all events for a specific user (/me alias)","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"$ref":"#/components/parameters/calendar.Top"},{"$ref":"#/components/parameters/calendar.Skip"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Alias for $top — max items per page (default 50)"}],"responses":{"200":{"description":"User events","content":{"application/json":{"schema":{"type":"object","properties":{"value":{"type":"array","items":{"$ref":"#/components/schemas/calendar.Event"}},"@odata.nextLink":{"type":"string","nullable":true,"description":"OData next-page link, absent on the last page"}}}}}}},"x-mcp":false,"description":"Alias of /users/{email}/events: acts as the impersonated user (X-PS-Impersonate-User header, or the instance's default MCP user).","tags":["calendar"]},"post":{"x-mcp":false,"summary":"Create event (user-scoped path) (/me alias)","description":"User-scoped twin of POST /events. Defaults the organizer to the resolved user when the body doesn't set one. Alias of /users/{email}/events: acts as the impersonated user (X-PS-Impersonate-User header, or the instance's default MCP user).","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.Event"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.Event"}}}}},"tags":["calendar"]}},"/purple-suite/calendar/me/events/{id}":{"get":{"x-mcp":false,"summary":"Get event (user-scoped path) (/me alias)","description":"User-scoped twin of GET /events/{id}. Alias of /users/{email}/events/{id}: acts as the impersonated user (X-PS-Impersonate-User header, or the instance's default MCP user).","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.Event"}}}},"404":{"description":"Not found"}},"tags":["calendar"]},"patch":{"x-mcp":false,"summary":"Update event (user-scoped path) (/me alias)","description":"User-scoped twin of PATCH /events/{id}. Alias of /users/{email}/events/{id}: acts as the impersonated user (X-PS-Impersonate-User header, or the instance's default MCP user).","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.Event"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.Event"}}}},"404":{"description":"Not found"}},"tags":["calendar"]},"delete":{"x-mcp":false,"summary":"Delete event (user-scoped path) (/me alias)","description":"User-scoped twin of DELETE /events/{id}. Alias of /users/{email}/events/{id}: acts as the impersonated user (X-PS-Impersonate-User header, or the instance's default MCP user).","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No content"},"404":{"description":"Not found"}},"tags":["calendar"]}},"/purple-suite/calendar/me/calendars":{"get":{"x-mcp":false,"summary":"List all calendars (user-scoped alias — returns all instance calendars) (/me alias)","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"$ref":"#/components/parameters/calendar.Top"},{"$ref":"#/components/parameters/calendar.Skip"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Alias for $top — max items per page (default 50)"}],"responses":{"200":{"description":"Collection of calendars","content":{"application/json":{"schema":{"type":"object","properties":{"value":{"type":"array","items":{"$ref":"#/components/schemas/calendar.Calendar"}},"@odata.nextLink":{"type":"string","nullable":true,"description":"OData next-page link, absent on the last page"}}}}}}},"description":"Alias of /users/{email}/calendars: acts as the impersonated user (X-PS-Impersonate-User header, or the instance's default MCP user).","tags":["calendar"]},"post":{"x-mcp":false,"summary":"Create calendar (user-scoped path) (/me alias)","description":"User-scoped twin of POST /calendars. Defaults the owner to the resolved user when the body doesn't set one. Alias of /users/{email}/calendars: acts as the impersonated user (X-PS-Impersonate-User header, or the instance's default MCP user).","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.Calendar"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.Calendar"}}}}},"tags":["calendar"]}},"/purple-suite/calendar/me/calendars/{id}":{"get":{"x-mcp":false,"summary":"Get calendar (user-scoped path) (/me alias)","description":"User-scoped twin of GET /calendars/{id}. Alias of /users/{email}/calendars/{id}: acts as the impersonated user (X-PS-Impersonate-User header, or the instance's default MCP user).","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.Calendar"}}}},"404":{"description":"Not found"}},"tags":["calendar"]},"patch":{"x-mcp":false,"summary":"Update calendar (user-scoped path) (/me alias)","description":"User-scoped twin of PATCH /calendars/{id}. Alias of /users/{email}/calendars/{id}: acts as the impersonated user (X-PS-Impersonate-User header, or the instance's default MCP user).","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.Calendar"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.Calendar"}}}},"404":{"description":"Not found"}},"tags":["calendar"]},"delete":{"x-mcp":false,"summary":"Delete calendar (user-scoped path) (/me alias)","description":"User-scoped twin of DELETE /calendars/{id}. Alias of /users/{email}/calendars/{id}: acts as the impersonated user (X-PS-Impersonate-User header, or the instance's default MCP user).","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No content"},"404":{"description":"Not found"}},"tags":["calendar"]}},"/purple-suite/calendar/me/calendars/{id}/events":{"get":{"summary":"List events for a specific calendar (user-scoped path) (/me alias)","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"},"description":"Calendar id"},{"in":"query","name":"startDateTime","schema":{"type":"string","format":"date-time"},"example":"2026-06-02T00:00:00Z"},{"in":"query","name":"endDateTime","schema":{"type":"string","format":"date-time"},"example":"2026-06-06T23:59:59Z"}],"responses":{"200":{"description":"Events for the calendar","content":{"application/json":{"schema":{"type":"object","properties":{"value":{"type":"array","items":{"$ref":"#/components/schemas/calendar.Event"}}}}}}},"404":{"description":"Calendar not found"}},"x-mcp":false,"description":"Alias of /users/{email}/calendars/{id}/events: acts as the impersonated user (X-PS-Impersonate-User header, or the instance's default MCP user).","tags":["calendar"]},"post":{"summary":"Create event in a specific calendar (user-scoped path) (/me alias)","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"},"description":"Calendar id"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.Event"}}}},"responses":{"201":{"description":"Created event","content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.Event"}}}},"404":{"description":"Calendar not found"}},"x-mcp":false,"description":"Alias of /users/{email}/calendars/{id}/events: acts as the impersonated user (X-PS-Impersonate-User header, or the instance's default MCP user).","tags":["calendar"]}},"/purple-suite/calendar/me/calendar/getSchedule":{"post":{"summary":"Get free/busy schedule for users (user-scoped path) (/me alias)","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"schedules":{"type":"array","items":{"type":"string","format":"email"},"description":"SMTP addresses to query","example":["alice.chen@acme.com","bob.martinez@acme.com"]},"startTime":{"allOf":[{"$ref":"#/components/schemas/calendar.DateTimeTimeZone"}],"example":{"dateTime":"2026-06-02T08:00:00.0000000","timeZone":"Pacific Standard Time"}},"endTime":{"allOf":[{"$ref":"#/components/schemas/calendar.DateTimeTimeZone"}],"example":{"dateTime":"2026-06-06T18:00:00.0000000","timeZone":"Pacific Standard Time"}},"availabilityViewInterval":{"type":"integer","default":30,"description":"Minutes per availability slot","example":30}},"required":["schedules","startTime","endTime"]}}}},"responses":{"200":{"description":"Schedule information","content":{"application/json":{"schema":{"type":"object","properties":{"value":{"type":"array","items":{"$ref":"#/components/schemas/calendar.ScheduleInformation"}}}}}}}},"x-mcp":false,"description":"Alias of /users/{email}/calendar/getSchedule: acts as the impersonated user (X-PS-Impersonate-User header, or the instance's default MCP user).","tags":["calendar"]}},"/purple-suite/calendar/me/mailboxSettings":{"get":{"summary":"Get user mailbox settings (/me alias)","description":"Returns a Graph-compatible MailboxSettings projection of the stored UserSettings record. Returns sensible defaults for unknown users. Alias of /users/{email}/mailboxSettings: acts as the impersonated user (X-PS-Impersonate-User header, or the instance's default MCP user).","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"}],"responses":{"200":{"description":"Mailbox settings","content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.MailboxSettings"}}}}},"x-mcp":false,"tags":["calendar"]},"patch":{"summary":"Update user mailbox settings (/me alias)","description":"Merges the provided fields into the user's settings. Creates a record if none exists. Validates timeZone and workingHours. Alias of /users/{email}/mailboxSettings: acts as the impersonated user (X-PS-Impersonate-User header, or the instance's default MCP user).","parameters":[{"$ref":"#/components/parameters/calendar.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.MailboxSettings"}}}},"responses":{"200":{"description":"Updated mailbox settings","content":{"application/json":{"schema":{"$ref":"#/components/schemas/calendar.MailboxSettings"}}}},"422":{"description":"Invalid timeZone or workingHours"}},"x-mcp":false,"tags":["calendar"]}},"/purple-suite/chat/channels":{"get":{"summary":"List Purple Collab channels","description":"Public and private channels in the Purple Collab chat workspace (the Slack-style team messaging system — not an ITSM queue or a community forum). Use it to resolve a channel name to the channel ID that messages reference. Supports server-side querying with _filter/_search/_orderby/_select/_top/_skip/_count (OData $-params, renamed with a leading underscore for MCP).","parameters":[{"$ref":"#/components/parameters/chat.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/chat.Channel"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["chat"]},"post":{"summary":"Create a Purple Collab channel","description":"Creates a new public or private channel in the Purple Collab workspace. Use only when the user wants a brand-new channel; to post into an existing one, look up its ID with the channel list tool and create a message instead.","parameters":[{"$ref":"#/components/parameters/chat.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/chat.Channel"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/chat.Channel"}}}}},"tags":["chat"]}},"/purple-suite/chat/channels/{id}":{"get":{"summary":"Get channel","parameters":[{"$ref":"#/components/parameters/chat.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/chat.Channel"}}}},"404":{"description":"Not found"}},"tags":["chat"]},"patch":{"summary":"Update channel","parameters":[{"$ref":"#/components/parameters/chat.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/chat.Channel"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/chat.Channel"}}}},"404":{"description":"Not found"}},"tags":["chat"]},"delete":{"summary":"Delete channel","parameters":[{"$ref":"#/components/parameters/chat.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["chat"]}},"/purple-suite/chat/messages":{"get":{"summary":"List Purple Collab chat messages","description":"Messages posted in Purple Collab channels, including thread replies. Pass channelId to read one channel's history or parent_message_id to read a single thread. Supports server-side querying with _filter/_search/_orderby/_select/_top/_skip/_count (OData $-params, renamed with a leading underscore for MCP).","parameters":[{"$ref":"#/components/parameters/chat.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."},{"in":"query","name":"channelId","schema":{"type":"string","example":"CH-002"},"description":"Filter messages by channel ID"},{"in":"query","name":"parent_message_id","schema":{"type":"string","example":"MSG-00001"},"description":"Filter to thread replies of a specific parent message ID"}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/chat.Message"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["chat"]},"post":{"summary":"Post a message to a Collab channel","description":"Posts a new top-level message into a Purple Collab channel (channelId, author email and text). To answer inside an existing thread, use the message reply tool instead so the reply is linked to its parent.","parameters":[{"$ref":"#/components/parameters/chat.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/chat.Message"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/chat.Message"}}}}},"tags":["chat"]}},"/purple-suite/chat/messages/{id}":{"get":{"summary":"Get message","parameters":[{"$ref":"#/components/parameters/chat.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/chat.Message"}}}},"404":{"description":"Not found"}},"tags":["chat"]},"patch":{"summary":"Edit a Collab message or its reactions","description":"Partially updates an existing Purple Collab message — typically to edit its text or rewrite the emoji reactions map. Interactive-button state is not set here; use the message interact tool for that.","parameters":[{"$ref":"#/components/parameters/chat.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/chat.Message"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/chat.Message"}}}},"404":{"description":"Not found"}},"tags":["chat"]},"delete":{"summary":"Delete a Collab message","description":"Permanently removes a message from Purple Collab. Destructive and irreversible — the text is gone and any thread replies are orphaned; prefer editing the message when the user only wants to change what it says.","parameters":[{"$ref":"#/components/parameters/chat.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["chat"]}},"/purple-suite/chat/scheduled-messages":{"get":{"summary":"List scheduled messages","parameters":[{"$ref":"#/components/parameters/chat.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."},{"in":"query","name":"channelId","schema":{"type":"string","example":"CH-002"},"description":"Filter scheduled messages by channel ID"}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/chat.ScheduledMessage"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["chat"]},"post":{"summary":"Create scheduled message","parameters":[{"$ref":"#/components/parameters/chat.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/chat.ScheduledMessage"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/chat.ScheduledMessage"}}}}},"tags":["chat"]}},"/purple-suite/chat/scheduled-messages/{id}":{"get":{"summary":"Get scheduled message","parameters":[{"$ref":"#/components/parameters/chat.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/chat.ScheduledMessage"}}}},"404":{"description":"Not found"}},"tags":["chat"]},"patch":{"summary":"Update scheduled message","parameters":[{"$ref":"#/components/parameters/chat.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/chat.ScheduledMessage"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/chat.ScheduledMessage"}}}},"404":{"description":"Not found"}},"tags":["chat"]},"delete":{"summary":"Delete scheduled message","parameters":[{"$ref":"#/components/parameters/chat.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["chat"]}},"/purple-suite/chat/drafts":{"get":{"summary":"List drafts","parameters":[{"$ref":"#/components/parameters/chat.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."},{"in":"query","name":"channelId","schema":{"type":"string","example":"CH-002"},"description":"Filter drafts by channel ID"}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/chat.Draft"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["chat"]},"post":{"summary":"Create draft","parameters":[{"$ref":"#/components/parameters/chat.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/chat.Draft"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/chat.Draft"}}}}},"tags":["chat"]}},"/purple-suite/chat/drafts/{id}":{"get":{"summary":"Get draft","parameters":[{"$ref":"#/components/parameters/chat.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/chat.Draft"}}}},"404":{"description":"Not found"}},"tags":["chat"]},"patch":{"summary":"Update draft","parameters":[{"$ref":"#/components/parameters/chat.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/chat.Draft"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/chat.Draft"}}}},"404":{"description":"Not found"}},"tags":["chat"]},"delete":{"summary":"Delete draft","parameters":[{"$ref":"#/components/parameters/chat.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["chat"]}},"/purple-suite/chat/messages/{id}/reply":{"post":{"summary":"Reply in a Collab message thread","description":"Posts a threaded reply under an existing Purple Collab message: the new message gets parent_message_id set and the parent's thread_count is incremented. Use this rather than creating a plain message whenever the user is responding inside a thread.","parameters":[{"$ref":"#/components/parameters/chat.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string","example":"MSG-00001"},"description":"Document ID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"All provided fields are spread into the reply message document. id, parent_message_id, thread_count, createdAt, and updatedAt are set automatically.","properties":{"channelId":{"type":"string","example":"CH-002","description":"ID of the channel this reply belongs to"},"author":{"type":"string","example":"bob.martinez@acme.com","description":"Email of the user posting the reply"},"text":{"type":"string","example":"Good catch — just fixed it in the latest commit.","description":"Reply message text"},"reactions":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}},"example":{},"description":"Initial reactions map (emoji → list of user emails); optional"},"editedAt":{"type":"string","format":"date-time","nullable":true,"example":null,"description":"Timestamp of last edit; optional, defaults to null"}},"required":["channelId","author","text"]}}}},"responses":{"201":{"description":"Reply message created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/chat.Message"}}}},"404":{"description":"Parent message not found"}},"tags":["chat"]}},"/purple-suite/chat/messages/{id}/interact":{"post":{"summary":"Click a button on a Collab message","description":"Records an interactive block-kit style action on a Purple Collab message: sets interactive_state[action_id] to the supplied value and appends to the append-only interactions log. Use it to answer an approve/choose prompt on a bot message; use the message update tool for text or reaction edits.","parameters":[{"$ref":"#/components/parameters/chat.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string","example":"MSG-00001"},"description":"Document ID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"action_id":{"type":"string","example":"approve_button","description":"Identifier of the interactive element that was triggered"},"value":{"example":"approved","description":"New value to set for the action (any JSON-serialisable value)"}},"required":["action_id"]}}}},"responses":{"200":{"description":"Updated message after interaction","content":{"application/json":{"schema":{"$ref":"#/components/schemas/chat.Message"}}}},"400":{"description":"action_id is required"},"404":{"description":"Message not found"}},"tags":["chat"]}},"/purple-suite/chat/users":{"get":{"summary":"List Purple Collab workspace members","description":"People with a chat account in the Purple Collab workspace — display name, email, presence (active/away/offline/do_not_disturb), chat role and timezone. These are chat accounts, not HR employee records (see the hris app) or the canonical people directory (see the identity app). Supports server-side querying with _filter/_search/_orderby/_select/_top/_skip/_count (OData $-params, renamed with a leading underscore for MCP).","parameters":[{"$ref":"#/components/parameters/chat.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/chat.User"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["chat"]},"post":{"summary":"Create user","parameters":[{"$ref":"#/components/parameters/chat.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/chat.User"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/chat.User"}}}}},"tags":["chat"]}},"/purple-suite/chat/users/{id}":{"get":{"summary":"Get user","parameters":[{"$ref":"#/components/parameters/chat.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/chat.User"}}}},"404":{"description":"Not found"}},"tags":["chat"]},"patch":{"summary":"Update user","parameters":[{"$ref":"#/components/parameters/chat.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/chat.User"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/chat.User"}}}},"404":{"description":"Not found"}},"tags":["chat"]},"delete":{"summary":"Delete user","parameters":[{"$ref":"#/components/parameters/chat.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["chat"]}},"/purple-suite/chat/files":{"get":{"summary":"List files","parameters":[{"$ref":"#/components/parameters/chat.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/chat.File"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["chat"]},"post":{"summary":"Create file","parameters":[{"$ref":"#/components/parameters/chat.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/chat.File"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/chat.File"}}}}},"tags":["chat"]}},"/purple-suite/chat/files/{id}":{"get":{"summary":"Get file","parameters":[{"$ref":"#/components/parameters/chat.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/chat.File"}}}},"404":{"description":"Not found"}},"tags":["chat"]},"patch":{"summary":"Update file","parameters":[{"$ref":"#/components/parameters/chat.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/chat.File"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/chat.File"}}}},"404":{"description":"Not found"}},"tags":["chat"]},"delete":{"summary":"Delete file","parameters":[{"$ref":"#/components/parameters/chat.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["chat"]}},"/purple-suite/community/feature-requests":{"get":{"summary":"List feature requests for a session","description":"Returns the feature_requests array stored on the given session document. Public endpoint — no PAT required.","security":[],"parameters":[{"in":"query","name":"sessionId","required":true,"schema":{"type":"string"},"example":"sess_abc123","description":"The session document ID whose feature_requests array should be returned"}],"responses":{"200":{"description":"Array of feature request objects stored on the session","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/community.LegacyFeatureRequest"}}}}},"400":{"description":"sessionId query parameter is missing"},"404":{"description":"Session not found"}},"tags":["community"]}},"/purple-suite/community/feature-requests/{id}":{"patch":{"summary":"Update the status of a session feature request","description":"Maps newStatusNumber to a human-readable status string and writes it to current_status on the matching feature request in the session's feature_requests array.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"},"example":"fr-0001","description":"The id of the feature request inside the session's feature_requests array"},{"in":"query","name":"sessionId","required":true,"schema":{"type":"string"},"example":"sess_abc123","description":"The session document ID that owns the feature request"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/community.UpdateFeatureRequestStatusBody"}}}},"responses":{"200":{"description":"Feature request updated","content":{"text/plain":{"schema":{"type":"string","example":"Feature request updated"}}}},"400":{"description":"Missing required parameters or invalid newStatusNumber"},"404":{"description":"Session or feature request not found"}},"tags":["community"]}},"/purple-suite/community/feature_requests":{"get":{"summary":"List community feature requests (the customer product-ideas board)","parameters":[{"$ref":"#/components/parameters/community.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/community.FeatureRequest"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"description":"Customer-submitted product ideas for this instance, with name, productArea (UI/UX, Performance, Security, Integrations, API, Analytics, Admin, Notifications), currentStatus New → Under Review → Planned → In Progress → Delivered/Declined, vote count, submitter and moderator. This is the public wish-list board — committed delivery plans live in the roadmap app and support tickets in itsm. Supports server-side OData querying — _filter, _orderby, _select, _search, _top, _skip, _count (the spec names these $filter etc.; MCP clients see the _ form because $ is not a legal tool-input key).","tags":["community"]},"post":{"summary":"Submit a community feature request (new product idea)","parameters":[{"$ref":"#/components/parameters/community.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/community.FeatureRequest"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/community.FeatureRequest"}}}}},"description":"Adds an idea to the community board with a name, product area, submitter and starting status. Use it for wish-list requests only — bugs and support issues belong in the itsm app.","tags":["community"]}},"/purple-suite/community/feature_requests/{id}":{"get":{"summary":"Get feature request","parameters":[{"$ref":"#/components/parameters/community.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/community.FeatureRequest"}}}},"404":{"description":"Not found"}},"tags":["community"]},"patch":{"summary":"Update a community feature request (triage status, votes, moderator)","description":"Partial update of one idea on the board — most often moving currentStatus through triage (New → Under Review → Planned → In Progress → Delivered/Declined), incrementing votes, or assigning a moderator. Send only the fields you want to change.","parameters":[{"$ref":"#/components/parameters/community.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/community.FeatureRequest"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/community.FeatureRequest"}}}},"404":{"description":"Not found"}},"tags":["community"]},"delete":{"summary":"Delete feature request","parameters":[{"$ref":"#/components/parameters/community.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["community"]}},"/purple-suite/crm/accounts":{"get":{"summary":"List CRM accounts (customer companies)","parameters":[{"$ref":"#/components/parameters/crm.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/crm.Account"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"description":"Sales-relationship records for companies this org sells to — customers, prospects and partners — with owner, territory, industry, ARR/MRR and renewal dates. These are not bank deposit accounts (see the retail-banking app) and not user/login accounts (see the identity app). Supports server-side OData querying — _filter, _orderby, _select, _search, _top, _skip, _count (the spec names these $filter etc.; MCP clients see the _ form because $ is not a legal tool-input key).","tags":["crm"]},"post":{"summary":"Create CRM account (customer company)","parameters":[{"$ref":"#/components/parameters/crm.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/crm.Account"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/crm.Account"}}}}},"description":"Adds a new company record to the CRM. Use this for an organisation you sell to; an individual person at a known company belongs in contacts, and an unqualified inbound person belongs in leads.","tags":["crm"]}},"/purple-suite/crm/accounts/{id}":{"get":{"summary":"Get CRM account by id","parameters":[{"$ref":"#/components/parameters/crm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/crm.Account"}}}},"404":{"description":"Not found"}},"description":"Fetches one customer company (e.g. ACC-0001) with its firmographics and revenue rollups. Use this when you already have the account id; to find an account by name or attribute, use the list tool with _search or _filter.","tags":["crm"]},"patch":{"summary":"Update CRM account","description":"Partial update of a customer company — e.g. reassign owner, change status (customer/prospect/partner/inactive), territory, or revenue fields. Send only the fields you want to change.","parameters":[{"$ref":"#/components/parameters/crm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/crm.Account"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/crm.Account"}}}},"404":{"description":"Not found"}},"tags":["crm"]},"delete":{"summary":"Delete CRM account (destructive)","parameters":[{"$ref":"#/components/parameters/crm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"description":"Permanently removes the company record; this cannot be undone and related contacts, opportunities and cases are not cascaded or cleaned up. Prefer updating status to 'inactive' unless the record genuinely must be purged.","tags":["crm"]}},"/purple-suite/crm/contacts":{"get":{"summary":"List CRM contacts (named people at customer accounts)","parameters":[{"$ref":"#/components/parameters/crm.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/crm.Contact"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"description":"External customer-side individuals — buyers, champions, admins — each linked to an account via accountId, with title, email, phone and active/inactive status. Not employees (see the hris and identity apps) and not unqualified prospects (see leads). Supports server-side OData querying — _filter, _orderby, _select, _search, _top, _skip, _count (the spec names these $filter etc.; MCP clients see the _ form because $ is not a legal tool-input key).","tags":["crm"]},"post":{"summary":"Create CRM contact (person at a customer account)","parameters":[{"$ref":"#/components/parameters/crm.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/crm.Contact"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/crm.Contact"}}}}},"description":"Adds a person who already belongs to a known account. For an unqualified inbound prospect create a lead instead, and to promote an existing lead use the lead convert tool rather than creating a contact by hand.","tags":["crm"]}},"/purple-suite/crm/contacts/{id}":{"get":{"summary":"Get CRM contact by id","parameters":[{"$ref":"#/components/parameters/crm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/crm.Contact"}}}},"404":{"description":"Not found"}},"description":"Fetches one customer-side person record by id, including their accountId, title and contact details.","tags":["crm"]},"patch":{"summary":"Update CRM contact","description":"Partial update of a customer-side person — e.g. new title, email, phone, owning account, or flipping status to inactive.","parameters":[{"$ref":"#/components/parameters/crm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/crm.Contact"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/crm.Contact"}}}},"404":{"description":"Not found"}},"tags":["crm"]},"delete":{"summary":"Delete CRM contact (destructive)","parameters":[{"$ref":"#/components/parameters/crm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"description":"Permanently removes the person record; this cannot be undone. Prefer setting status to 'inactive' so engagement history stays attributable.","tags":["crm"]}},"/purple-suite/crm/opportunities":{"get":{"summary":"List CRM opportunities (sales deals in the pipeline)","parameters":[{"$ref":"#/components/parameters/crm.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/crm.Opportunity"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"description":"Revenue deals tied to an account, with stage, amount, probability, closeDate, owner and a server-maintained stageHistory. This is the sales pipeline — customer support tickets are cases and logged touchpoints are activities. Supports server-side OData querying — _filter, _orderby, _select, _search, _top, _skip, _count (the spec names these $filter etc.; MCP clients see the _ form because $ is not a legal tool-input key). Example: _filter=\"stage ne 'closed_lost' and amount gt 50000\".","tags":["crm"]},"post":{"summary":"Create CRM opportunity (new sales deal)","parameters":[{"$ref":"#/components/parameters/crm.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/crm.Opportunity"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/crm.Opportunity"}}}}},"description":"Opens a new pipeline deal against an account with a stage, amount and close date. To move an existing deal forward use the advance-stage tool rather than creating a second opportunity.","tags":["crm"]}},"/purple-suite/crm/opportunities/{id}":{"get":{"summary":"Get CRM opportunity by id","parameters":[{"$ref":"#/components/parameters/crm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/crm.Opportunity"}}}},"404":{"description":"Not found"}},"description":"Fetches one pipeline deal including its stageHistory (the audit trail of stage moves recorded by the advance-stage tool).","tags":["crm"]},"patch":{"summary":"Update CRM opportunity (non-stage fields)","description":"Partial update of deal attributes — amount, probability, closeDate, owner, name. To change the pipeline stage prefer the advance-stage tool, which enforces the stage order and appends to stageHistory; this tool does neither.","parameters":[{"$ref":"#/components/parameters/crm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/crm.Opportunity"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/crm.Opportunity"}}}},"404":{"description":"Not found"}},"tags":["crm"]},"delete":{"summary":"Delete CRM opportunity (destructive)","parameters":[{"$ref":"#/components/parameters/crm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"description":"Permanently removes the deal and its stage history; this cannot be undone. To record a deal that did not land, advance its stage to closed_lost instead.","tags":["crm"]}},"/purple-suite/crm/opportunities/{id}/advance-stage":{"post":{"summary":"Advance a CRM opportunity to a new pipeline stage","description":"The preferred way to move a deal: sets the stage, appends a timestamped stageHistory entry with optional notes, and enforces pipeline order. Forward-only and terminal — moving backwards is rejected, and a closed_won/closed_lost deal cannot be advanced at all. Use PATCH /opportunities/{id} for non-stage edits.","parameters":[{"$ref":"#/components/parameters/crm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"stage":{"type":"string","enum":["prospecting","qualification","proposal","negotiation","closed_won","closed_lost"],"example":"proposal"},"notes":{"type":"string","example":"Customer responded positively to initial pitch"}},"required":["stage"]}}}},"responses":{"200":{"description":"Updated opportunity with new stage and stageHistory entry","content":{"application/json":{"schema":{"$ref":"#/components/schemas/crm.Opportunity"}}}},"400":{"description":"Invalid stage value"},"404":{"description":"Not found"},"422":{"description":"Stage transition not allowed (opportunity is closed or move is backwards)"}},"tags":["crm"]}},"/purple-suite/crm/leads":{"get":{"summary":"List CRM leads (unqualified inbound prospects)","parameters":[{"$ref":"#/components/parameters/crm.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/crm.Lead"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"description":"Top-of-funnel people not yet tied to an account, with source (web form, trade show, referral…), score and status new → contacted → qualified → converted/disqualified/dead. Once qualified, a lead becomes a contact (and optionally an opportunity) via the convert tool. Supports server-side OData querying — _filter, _orderby, _select, _search, _top, _skip, _count (the spec names these $filter etc.; MCP clients see the _ form because $ is not a legal tool-input key).","tags":["crm"]},"post":{"summary":"Create CRM lead (raw inbound prospect)","parameters":[{"$ref":"#/components/parameters/crm.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/crm.Lead"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/crm.Lead"}}}}},"description":"Captures a new unqualified prospect with their company, source and score. Use contacts instead when the person already belongs to a known customer account.","tags":["crm"]}},"/purple-suite/crm/leads/{id}":{"get":{"summary":"Get CRM lead by id","parameters":[{"$ref":"#/components/parameters/crm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/crm.Lead"}}}},"404":{"description":"Not found"}},"description":"Fetches one prospect record, including convertedTo once the lead has been run through the convert tool.","tags":["crm"]},"patch":{"summary":"Update CRM lead","description":"Partial update — e.g. move status from new to contacted or qualified, adjust score, or fill in phone/title/accountId. Do not set status to 'converted' here; use the convert tool so the contact (and optional opportunity) are actually created.","parameters":[{"$ref":"#/components/parameters/crm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/crm.Lead"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/crm.Lead"}}}},"404":{"description":"Not found"}},"tags":["crm"]},"delete":{"summary":"Delete CRM lead (destructive)","parameters":[{"$ref":"#/components/parameters/crm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"description":"Permanently removes the prospect record; this cannot be undone. Prefer status 'disqualified' or 'dead' so funnel conversion rates stay accurate.","tags":["crm"]}},"/purple-suite/crm/leads/{id}/convert":{"post":{"summary":"Convert a CRM lead into a contact (and optionally an opportunity)","description":"The one-way qualification step: creates a Contact from the lead's fields, additionally creates an Opportunity when opportunity_data is supplied, and stamps the lead as 'converted'. Not repeatable and not reversible — a lead that is already converted or disqualified is rejected.","parameters":[{"$ref":"#/components/parameters/crm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"opportunity_data":{"type":"object","description":"Optional. If provided, an Opportunity is created by spreading these fields.","properties":{"name":{"type":"string","example":"Enterprise New Business - Q3 2026"},"accountId":{"type":"string","example":"ACC-0001"},"stage":{"type":"string","enum":["prospecting","qualification","proposal","negotiation","closed_won","closed_lost"],"example":"prospecting"},"amount":{"type":"number","example":50000},"probability":{"type":"integer","example":10},"closeDate":{"type":"string","format":"date-time","example":"2026-09-30T00:00:00.000Z"},"owner":{"type":"string","example":"alice@example.com"}}}}}}}},"responses":{"200":{"description":"Converted lead with the created contact and optional opportunity","content":{"application/json":{"schema":{"type":"object","properties":{"contact":{"$ref":"#/components/schemas/crm.Contact"},"opportunity":{"$ref":"#/components/schemas/crm.Opportunity","nullable":true},"lead":{"$ref":"#/components/schemas/crm.Lead"}}}}}},"404":{"description":"Not found"},"422":{"description":"Lead cannot be converted (already converted or disqualified)"}},"tags":["crm"]}},"/purple-suite/crm/cases":{"get":{"summary":"List CRM cases (customer support tickets)","parameters":[{"$ref":"#/components/parameters/crm.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/crm.Case"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"description":"Issues reported by a customer against an account, with subject, priority, assignee and status open → in_progress → resolved → closed. These are customer-facing support cases; internal employee IT tickets live in the itsm app. Supports server-side OData querying — _filter, _orderby, _select, _search, _top, _skip, _count (the spec names these $filter etc.; MCP clients see the _ form because $ is not a legal tool-input key).","tags":["crm"]},"post":{"summary":"Create CRM case (customer support ticket)","parameters":[{"$ref":"#/components/parameters/crm.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/crm.Case"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/crm.Case"}}}}},"description":"Logs a new customer-reported issue against an account with a subject and priority. For an internal employee IT problem create an itsm incident instead.","tags":["crm"]}},"/purple-suite/crm/cases/{id}":{"get":{"summary":"Get CRM case by id","parameters":[{"$ref":"#/components/parameters/crm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/crm.Case"}}}},"404":{"description":"Not found"}},"description":"Fetches one customer support case with its status, priority, assignee and resolution timestamps.","tags":["crm"]},"patch":{"summary":"Update or transition a CRM case","description":"Partial update, and the only way to move a case through its lifecycle. State-machine rules are enforced: an 'open' case cannot jump straight to 'closed' (route it via in_progress or resolved), and reopening a 'closed' case requires reopenReason (alias reopen_reason).","parameters":[{"$ref":"#/components/parameters/crm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/crm.Case"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/crm.Case"}}}},"404":{"description":"Not found"},"422":{"description":"State-machine violation — e.g. closing an open case directly, or reopening without reopenReason"}},"tags":["crm"]},"delete":{"summary":"Delete CRM case (destructive)","parameters":[{"$ref":"#/components/parameters/crm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"description":"Permanently removes the support case and its history; this cannot be undone. Resolving or closing the case is almost always the right action instead.","tags":["crm"]}},"/purple-suite/crm/activities":{"get":{"summary":"List CRM activities (logged calls, emails, meetings, tasks, notes)","parameters":[{"$ref":"#/components/parameters/crm.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/crm.Activity"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"description":"The touchpoint timeline attached to an account: type, subject, owner, dueDate and a completed flag. Use it for engagement history or to find open follow-ups; real calendar invites live in the calendar app. Supports server-side OData querying — _filter, _orderby, _select, _search, _top, _skip, _count (the spec names these $filter etc.; MCP clients see the _ form because $ is not a legal tool-input key).","tags":["crm"]},"post":{"summary":"Log a CRM activity (call, email, meeting, task or note)","parameters":[{"$ref":"#/components/parameters/crm.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/crm.Activity"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/crm.Activity"}}}}},"description":"Records a touchpoint that happened, or schedules a follow-up task, against an account. This only writes a CRM record — it does not send an email or book a meeting.","tags":["crm"]}},"/purple-suite/crm/activities/{id}":{"get":{"summary":"Get CRM activity by id","parameters":[{"$ref":"#/components/parameters/crm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/crm.Activity"}}}},"404":{"description":"Not found"}},"description":"Fetches one logged touchpoint or follow-up task from an account's timeline.","tags":["crm"]},"patch":{"summary":"Update CRM activity","description":"Partial update of a logged touchpoint — most often flipping completed to true when a follow-up is done, or changing dueDate, owner or subject.","parameters":[{"$ref":"#/components/parameters/crm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/crm.Activity"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/crm.Activity"}}}},"404":{"description":"Not found"}},"tags":["crm"]},"delete":{"summary":"Delete CRM activity (destructive)","parameters":[{"$ref":"#/components/parameters/crm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"description":"Permanently removes the touchpoint from the account timeline; this cannot be undone. Mark a task completed instead of deleting it when the work simply finished.","tags":["crm"]}},"/purple-suite/crm/recent-campaign":{"get":{"summary":"Get most recent campaign (legacy stub)","description":"Legacy compatibility endpoint. Returns a static most-recent campaign record. No authentication required.","parameters":[],"responses":{"200":{"description":"Most recent campaign","content":{"application/json":{"schema":{"type":"object","properties":{"campaign":{"type":"object","properties":{"id":{"type":"string","example":"campaign-2025-q1"},"name":{"type":"string","example":"Q1 2025 Referral Campaign"},"status":{"type":"string","example":"active"}}}}}}}}},"security":[],"tags":["crm"]}},"/purple-suite/crm/engagement":{"get":{"summary":"Get customer engagement data (legacy stub)","description":"Legacy compatibility endpoint. Returns static engagement metrics. No authentication required.","parameters":[],"responses":{"200":{"description":"Engagement metrics","content":{"application/json":{"schema":{"type":"object","properties":{"engagement":{"type":"object","properties":{"totalUsers":{"type":"integer","example":1250},"activeUsers":{"type":"integer","example":890},"engagementRate":{"type":"number","example":71.2}}}}}}}}},"security":[],"tags":["crm"]}},"/purple-suite/dex/devices":{"get":{"summary":"List DEX-monitored endpoint devices with health telemetry","parameters":[{"$ref":"#/components/parameters/dex.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/dex.Device"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"description":"Managed endpoints (laptop, desktop, mobile, virtual_machine) and their live telemetry: health_score, CPU/memory/disk usage, battery health, uptime, last_seen, compliance_status, encryption and antivirus state, plus assigned_to, department and location. This is device-health monitoring (Nexthink/Lakeside style) — the finance-owned asset register lives in the erp app's assets collection, and user-reported device tickets live in itsm. Supports server-side OData querying — _filter, _orderby, _select, _search, _top, _skip, _count (the spec names these $filter etc.; MCP clients see the _ form because $ is not a legal tool-input key).","tags":["dex"]},"post":{"summary":"Create device","parameters":[{"$ref":"#/components/parameters/dex.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/dex.Device"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/dex.Device"}}}}},"tags":["dex"]}},"/purple-suite/dex/devices/{id}":{"get":{"summary":"Get device","parameters":[{"$ref":"#/components/parameters/dex.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/dex.Device"}}}},"404":{"description":"Not found"}},"tags":["dex"]},"patch":{"summary":"Update device","parameters":[{"$ref":"#/components/parameters/dex.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/dex.Device"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/dex.Device"}}}},"404":{"description":"Not found"}},"tags":["dex"]},"delete":{"summary":"Delete device","parameters":[{"$ref":"#/components/parameters/dex.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["dex"]}},"/purple-suite/dex/software_inventory":{"get":{"summary":"List software installed on DEX-monitored devices","parameters":[{"$ref":"#/components/parameters/dex.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/dex.SoftwareInventory"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"description":"One row per software title installed on a device (device_id plus software_name, version, publisher) with install_date, last_used, license_type and the is_managed / is_approved flags. Use it for licence-usage, shadow-IT (is_approved false) and version-spread questions — software purchasing and licence assets live in the erp app. Supports server-side OData querying — _filter, _orderby, _select, _search, _top, _skip, _count (the spec names these $filter etc.; MCP clients see the _ form because $ is not a legal tool-input key).","tags":["dex"]},"post":{"summary":"Create software inventory entry","parameters":[{"$ref":"#/components/parameters/dex.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/dex.SoftwareInventory"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/dex.SoftwareInventory"}}}}},"tags":["dex"]}},"/purple-suite/dex/software_inventory/{id}":{"get":{"summary":"Get software inventory entry","parameters":[{"$ref":"#/components/parameters/dex.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/dex.SoftwareInventory"}}}},"404":{"description":"Not found"}},"tags":["dex"]},"patch":{"summary":"Update software inventory entry","parameters":[{"$ref":"#/components/parameters/dex.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/dex.SoftwareInventory"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/dex.SoftwareInventory"}}}},"404":{"description":"Not found"}},"tags":["dex"]},"delete":{"summary":"Delete software inventory entry","parameters":[{"$ref":"#/components/parameters/dex.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["dex"]}},"/purple-suite/dex/dex_alerts":{"get":{"summary":"List DEX device-health alerts (machine-generated telemetry alerts)","parameters":[{"$ref":"#/components/parameters/dex.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/dex.DexAlert"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"description":"Automated endpoint alerts — high_cpu, low_disk, low_memory, crash, network_issue, battery_critical, security_threat, compliance_violation, software_error, hardware_failure — with severity, open/acknowledged/resolved status, and the metric_value that breached its threshold. These are generated from device telemetry, not raised by users (user-reported tickets live in the itsm app); the remediations collection records what was done about them. Supports server-side OData querying — _filter, _orderby, _select, _search, _top, _skip, _count (the spec names these $filter etc.; MCP clients see the _ form because $ is not a legal tool-input key).","tags":["dex"]},"post":{"summary":"Create DEX alert","parameters":[{"$ref":"#/components/parameters/dex.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/dex.DexAlert"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/dex.DexAlert"}}}}},"tags":["dex"]}},"/purple-suite/dex/dex_alerts/{id}":{"get":{"summary":"Get DEX alert","parameters":[{"$ref":"#/components/parameters/dex.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/dex.DexAlert"}}}},"404":{"description":"Not found"}},"tags":["dex"]},"patch":{"summary":"Update DEX alert","parameters":[{"$ref":"#/components/parameters/dex.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/dex.DexAlert"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/dex.DexAlert"}}}},"404":{"description":"Not found"}},"tags":["dex"]},"delete":{"summary":"Delete DEX alert","parameters":[{"$ref":"#/components/parameters/dex.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["dex"]}},"/purple-suite/dex/dex_alerts/{id}/acknowledge":{"post":{"summary":"Acknowledge a DEX alert","description":"Marks the alert as acknowledged. Fails if the alert is already resolved or already acknowledged.","parameters":[{"$ref":"#/components/parameters/dex.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"acknowledged_by":{"type":"string","description":"Email of the person acknowledging the alert","example":"admin@acme.com"}},"required":["acknowledged_by"]}}}},"responses":{"200":{"description":"Alert acknowledged","content":{"application/json":{"schema":{"$ref":"#/components/schemas/dex.DexAlert"}}}},"404":{"description":"Alert not found"},"422":{"description":"Alert already resolved or already acknowledged"}},"tags":["dex"]}},"/purple-suite/dex/dex_alerts/{id}/resolve":{"post":{"summary":"Resolve a DEX alert","description":"Marks the alert as resolved. Fails if the alert is already resolved.","parameters":[{"$ref":"#/components/parameters/dex.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"resolved_by":{"type":"string","description":"Email of the person resolving the alert","example":"admin@acme.com"},"resolution_notes":{"type":"string","description":"Optional notes about the resolution","example":"Restarted service, CPU normalized."}},"required":["resolved_by"]}}}},"responses":{"200":{"description":"Alert resolved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/dex.DexAlert"}}}},"404":{"description":"Alert not found"},"422":{"description":"Alert already resolved"}},"tags":["dex"]}},"/purple-suite/dex/remediations":{"get":{"summary":"List DEX remediation actions run against devices","parameters":[{"$ref":"#/components/parameters/dex.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/dex.Remediation"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"description":"Automated fix-it actions targeting a device (restart_service, clear_cache, force_update, restart_device, run_diagnostics, reset_network, update_drivers, scan_malware, enforce_compliance…) with status pending/in_progress/completed/failed, executor, timings and result. Use it to see what has already been attempted for a device or alert; the execute and complete tools are what drive the status. Supports server-side OData querying — _filter, _orderby, _select, _search, _top, _skip, _count (the spec names these $filter etc.; MCP clients see the _ form because $ is not a legal tool-input key).","tags":["dex"]},"post":{"summary":"Create remediation","parameters":[{"$ref":"#/components/parameters/dex.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/dex.Remediation"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/dex.Remediation"}}}}},"tags":["dex"]}},"/purple-suite/dex/remediations/{id}":{"get":{"summary":"Get remediation","parameters":[{"$ref":"#/components/parameters/dex.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/dex.Remediation"}}}},"404":{"description":"Not found"}},"tags":["dex"]},"patch":{"summary":"Update remediation","parameters":[{"$ref":"#/components/parameters/dex.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/dex.Remediation"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/dex.Remediation"}}}},"404":{"description":"Not found"}},"tags":["dex"]},"delete":{"summary":"Delete remediation","parameters":[{"$ref":"#/components/parameters/dex.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["dex"]}},"/purple-suite/dex/remediations/{id}/execute":{"post":{"summary":"Execute a remediation","description":"Transitions the remediation to in_progress and records the executor. Fails if already completed or in_progress. Failed remediations may be re-executed.","parameters":[{"$ref":"#/components/parameters/dex.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"executed_by":{"type":"string","description":"Email or identifier of the executor (defaults to 'system')","example":"admin@acme.com"}}}}}},"responses":{"200":{"description":"Remediation started (status = in_progress)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/dex.Remediation"}}}},"404":{"description":"Remediation not found"},"422":{"description":"Remediation already completed or already in_progress"}},"tags":["dex"]}},"/purple-suite/dex/remediations/{id}/complete":{"post":{"summary":"Mark a remediation as complete","description":"Transitions the remediation to completed. Remediation must be in_progress.","parameters":[{"$ref":"#/components/parameters/dex.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"result":{"type":"string","description":"Outcome description (defaults to 'success')","example":"success"}}}}}},"responses":{"200":{"description":"Remediation completed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/dex.Remediation"}}}},"404":{"description":"Remediation not found"},"422":{"description":"Remediation is not in_progress (must execute first)"}},"tags":["dex"]}},"/purple-suite/dex/experience_scores":{"get":{"summary":"List DEX digital-experience scores (0–10) per device and user","parameters":[{"$ref":"#/components/parameters/dex.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/dex.ExperienceScore"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"description":"Periodic scorecards that roll boot time, responsiveness, network, stability and battery into an overall_score for a device_id / user_email at a measured_at timestamp, alongside the raw boot_time_seconds, crash count and response-time inputs. Use it for experience trends and worst-experience rankings; point-in-time device telemetry lives on devices instead. Supports server-side OData querying — _filter, _orderby, _select, _search, _top, _skip, _count (the spec names these $filter etc.; MCP clients see the _ form because $ is not a legal tool-input key).","tags":["dex"]},"post":{"summary":"Create experience score","parameters":[{"$ref":"#/components/parameters/dex.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/dex.ExperienceScore"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/dex.ExperienceScore"}}}}},"tags":["dex"]}},"/purple-suite/dex/experience_scores/{id}":{"get":{"summary":"Get experience score","parameters":[{"$ref":"#/components/parameters/dex.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/dex.ExperienceScore"}}}},"404":{"description":"Not found"}},"tags":["dex"]},"patch":{"summary":"Update experience score","parameters":[{"$ref":"#/components/parameters/dex.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/dex.ExperienceScore"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/dex.ExperienceScore"}}}},"404":{"description":"Not found"}},"tags":["dex"]},"delete":{"summary":"Delete experience score","parameters":[{"$ref":"#/components/parameters/dex.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["dex"]}},"/purple-suite/dex/dex_campaigns":{"get":{"summary":"List DEX IT campaigns (fleet patch, rollout and compliance sweeps)","parameters":[{"$ref":"#/components/parameters/dex.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/dex.Campaign"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"description":"Fleet-wide IT operations targeting a device cohort — patch_deployment, software_rollout, compliance_check, security_scan, driver_update, os_upgrade — with target_criteria, status draft/active/completed and total/completed/failed device counts. These are IT rollout campaigns, not marketing campaigns (see the crm app). Supports server-side OData querying — _filter, _orderby, _select, _search, _top, _skip, _count (the spec names these $filter etc.; MCP clients see the _ form because $ is not a legal tool-input key).","tags":["dex"]},"post":{"summary":"Create DEX campaign","parameters":[{"$ref":"#/components/parameters/dex.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/dex.Campaign"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/dex.Campaign"}}}}},"tags":["dex"]}},"/purple-suite/dex/dex_campaigns/{id}":{"get":{"summary":"Get DEX campaign","parameters":[{"$ref":"#/components/parameters/dex.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/dex.Campaign"}}}},"404":{"description":"Not found"}},"tags":["dex"]},"patch":{"summary":"Update DEX campaign","parameters":[{"$ref":"#/components/parameters/dex.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/dex.Campaign"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/dex.Campaign"}}}},"404":{"description":"Not found"}},"tags":["dex"]},"delete":{"summary":"Delete DEX campaign","parameters":[{"$ref":"#/components/parameters/dex.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["dex"]}},"/purple-suite/dex/dex_campaigns/{id}/launch":{"post":{"summary":"Launch a DEX campaign","description":"Transitions the campaign to active and records launch metadata. Fails if already active or completed.","parameters":[{"$ref":"#/components/parameters/dex.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"launched_by":{"type":"string","description":"Email or identifier of the person launching the campaign (defaults to 'system')","example":"admin@acme.com"}}}}}},"responses":{"200":{"description":"Campaign launched (status = active)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/dex.Campaign"}}}},"404":{"description":"Campaign not found"},"422":{"description":"Campaign already active or already completed"}},"tags":["dex"]}},"/purple-suite/erp/purchase-orders":{"get":{"summary":"List ERP purchase orders (procurement commitments to vendors)","parameters":[{"$ref":"#/components/parameters/erp.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/erp.PurchaseOrder"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"description":"POs the company has raised against a vendor, with totalAmount, currency, lineItems, approver, expectedDelivery and status draft → pending_approval → approved → sent → partially_received → received (or cancelled). Vendor bills owed against these POs are invoices; employee out-of-pocket spend lives in the expense app. Supports server-side OData querying — _filter, _orderby, _select, _search, _top, _skip, _count (the spec names these $filter etc.; MCP clients see the _ form because $ is not a legal tool-input key).","tags":["erp"]},"post":{"summary":"Create ERP purchase order (raise a new PO to a vendor)","parameters":[{"$ref":"#/components/parameters/erp.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/erp.PurchaseOrder"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/erp.PurchaseOrder"}}}}},"description":"Raises a new procurement commitment against a vendor, normally in draft or pending_approval. Use invoices for a bill you have already received, and the verify-match tool to reconcile a PO against its invoice and goods receipt.","tags":["erp"]}},"/purple-suite/erp/purchase-orders/{id}":{"get":{"summary":"Get purchase order","parameters":[{"$ref":"#/components/parameters/erp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/erp.PurchaseOrder"}}}},"404":{"description":"Not found"}},"tags":["erp"]},"patch":{"summary":"Update or transition an ERP purchase order","description":"Partial update, and the way to move PO status through its lifecycle. State-machine rules are enforced: a cancelled PO cannot be modified at all, a fully 'received' PO cannot be cancelled, and only draft/pending_approval POs can be approved.","parameters":[{"$ref":"#/components/parameters/erp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/erp.PurchaseOrder"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/erp.PurchaseOrder"}}}},"404":{"description":"Not found"}},"tags":["erp"]},"delete":{"summary":"Delete purchase order","parameters":[{"$ref":"#/components/parameters/erp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["erp"]}},"/purple-suite/erp/purchase-orders/{id}/verify-match":{"post":{"summary":"3-way match verification","parameters":[{"$ref":"#/components/parameters/erp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"invoice_id":{"type":"string","example":"INV-0001"},"receipt_confirmed":{"type":"boolean","example":true}},"required":["invoice_id","receipt_confirmed"]}}}},"responses":{"200":{"description":"Match result","content":{"application/json":{"schema":{"type":"object","properties":{"matched":{"type":"boolean"},"po_id":{"type":"string"},"invoice_id":{"type":"string"},"status":{"type":"string"}}}}}},"404":{"description":"Not found"},"422":{"description":"Match validation failed"}},"tags":["erp"]}},"/purple-suite/erp/invoices":{"get":{"summary":"List ERP vendor invoices (accounts payable)","parameters":[{"$ref":"#/components/parameters/erp.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/erp.Invoice"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"description":"Bills the company owes suppliers, with vendor, amount, currency, dueDate, an optional poId link for 3-way match, and status draft → pending_payment → paid (or void / overdue). These are supplier bills — employee expense reports live in the expense app and customer billing is not modelled here. Supports server-side OData querying — _filter, _orderby, _select, _search, _top, _skip, _count (the spec names these $filter etc.; MCP clients see the _ form because $ is not a legal tool-input key).","tags":["erp"]},"post":{"summary":"Create ERP vendor invoice (record a bill in accounts payable)","parameters":[{"$ref":"#/components/parameters/erp.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/erp.Invoice"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/erp.Invoice"}}}}},"description":"Records a bill received from a vendor. Set poId to link it to the originating purchase order so the PO's verify-match tool can reconcile PO, invoice and receipt.","tags":["erp"]}},"/purple-suite/erp/invoices/{id}":{"get":{"summary":"Get invoice","parameters":[{"$ref":"#/components/parameters/erp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/erp.Invoice"}}}},"404":{"description":"Not found"}},"tags":["erp"]},"patch":{"summary":"Update or transition an ERP vendor invoice","description":"Partial update, and the way to move invoice status (e.g. pending_payment → paid). State-machine rules are enforced: a paid invoice cannot be voided (issue a credit memo or a reversal journal entry instead), a voided invoice cannot be marked paid, and a voided invoice is read-only apart from notes.","parameters":[{"$ref":"#/components/parameters/erp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/erp.Invoice"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/erp.Invoice"}}}},"404":{"description":"Not found"}},"tags":["erp"]},"delete":{"summary":"Delete invoice","parameters":[{"$ref":"#/components/parameters/erp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["erp"]}},"/purple-suite/erp/inventory":{"get":{"summary":"List ERP inventory items (stocked goods by SKU)","parameters":[{"$ref":"#/components/parameters/erp.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/erp.InventoryItem"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"description":"On-hand stock records — itemName, sku, quantity, reorderPoint, unitCost and warehouseLocation. Use it for stock-on-hand and reorder questions; capitalised equipment is tracked in assets, buying more stock goes through purchase-orders, and live device telemetry lives in the dex app. Supports server-side OData querying — _filter, _orderby, _select, _search, _top, _skip, _count (the spec names these $filter etc.; MCP clients see the _ form because $ is not a legal tool-input key).","tags":["erp"]},"post":{"summary":"Create ERP inventory item (stock a new SKU)","parameters":[{"$ref":"#/components/parameters/erp.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/erp.InventoryItem"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/erp.InventoryItem"}}}}},"description":"Adds a stocked SKU with its opening quantity, reorder point, unit cost and warehouse location. For a capitalised piece of equipment create an asset instead.","tags":["erp"]}},"/purple-suite/erp/inventory/{id}":{"get":{"summary":"Get inventory item","parameters":[{"$ref":"#/components/parameters/erp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/erp.InventoryItem"}}}},"404":{"description":"Not found"}},"tags":["erp"]},"patch":{"summary":"Update inventory item","parameters":[{"$ref":"#/components/parameters/erp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/erp.InventoryItem"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/erp.InventoryItem"}}}},"404":{"description":"Not found"}},"tags":["erp"]},"delete":{"summary":"Delete inventory item","parameters":[{"$ref":"#/components/parameters/erp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["erp"]}},"/purple-suite/erp/journal-entries":{"get":{"summary":"List ERP general-ledger journal entries","parameters":[{"$ref":"#/components/parameters/erp.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/erp.JournalEntry"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"description":"Double-entry accounting records — type (debit/credit/adjustment/accrual/reversal), debitAccount, creditAccount, amount, accounting period and status draft → posted → reversed, plus reversalOf for entries created by a reversal. This is the GL; vendor bills are invoices and procurement commitments are purchase-orders. Supports server-side OData querying — _filter, _orderby, _select, _search, _top, _skip, _count (the spec names these $filter etc.; MCP clients see the _ form because $ is not a legal tool-input key).","tags":["erp"]},"post":{"summary":"Create ERP journal entry (book a GL debit/credit)","parameters":[{"$ref":"#/components/parameters/erp.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/erp.JournalEntry"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/erp.JournalEntry"}}}}},"description":"Books a new general-ledger entry for a period, normally as a draft that is later posted. To undo an already-posted entry use the reverse tool rather than editing or deleting the original.","tags":["erp"]}},"/purple-suite/erp/journal-entries/{id}":{"get":{"summary":"Get journal entry","parameters":[{"$ref":"#/components/parameters/erp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/erp.JournalEntry"}}}},"404":{"description":"Not found"}},"tags":["erp"]},"patch":{"summary":"Update or post an ERP journal entry","description":"Partial update, most commonly setting status to 'posted'. State-machine rules are enforced: reversed entries are read-only, and on a posted entry the financial fields (amount, debitAccount, creditAccount) cannot be changed — create a reversal instead.","parameters":[{"$ref":"#/components/parameters/erp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/erp.JournalEntry"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/erp.JournalEntry"}}}},"404":{"description":"Not found"}},"tags":["erp"]},"delete":{"summary":"Delete journal entry","parameters":[{"$ref":"#/components/parameters/erp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["erp"]}},"/purple-suite/erp/journal-entries/{id}/reverse":{"post":{"summary":"Reverse a journal entry","description":"Marks the entry as reversed and creates a new posted reversal entry with debit/credit swapped. Only posted entries can be reversed.","parameters":[{"$ref":"#/components/parameters/erp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"201":{"description":"Reversal entry created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/erp.JournalEntry"}}}},"404":{"description":"Not found"},"422":{"description":"Entry cannot be reversed in its current state"}},"tags":["erp"]}},"/purple-suite/erp/assets":{"get":{"summary":"List ERP fixed assets (the finance asset register)","parameters":[{"$ref":"#/components/parameters/erp.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/erp.Asset"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"description":"Capitalised assets the company owns — IT Equipment, Furniture, Vehicle, Machinery, Building, Software License — with purchaseValue, currentValue, purchasedAt, location and status active/maintenance/retired/disposed. This is the finance-of-record register; live endpoint health for IT devices is in the dex app and consumable stock is in inventory. Supports server-side OData querying — _filter, _orderby, _select, _search, _top, _skip, _count (the spec names these $filter etc.; MCP clients see the _ form because $ is not a legal tool-input key).","tags":["erp"]},"post":{"summary":"Create ERP fixed asset record","parameters":[{"$ref":"#/components/parameters/erp.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/erp.Asset"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/erp.Asset"}}}}},"description":"Registers a newly acquired capital asset with its type, purchase value and location. Consumable or resold stock belongs in inventory rather than here.","tags":["erp"]}},"/purple-suite/erp/assets/{id}":{"get":{"summary":"Get asset","parameters":[{"$ref":"#/components/parameters/erp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/erp.Asset"}}}},"404":{"description":"Not found"}},"tags":["erp"]},"patch":{"summary":"Update asset","parameters":[{"$ref":"#/components/parameters/erp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/erp.Asset"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/erp.Asset"}}}},"404":{"description":"Not found"}},"tags":["erp"]},"delete":{"summary":"Delete asset","parameters":[{"$ref":"#/components/parameters/erp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["erp"]}},"/purple-suite/erp/purchase-requests":{"post":{"summary":"Create purchase request (legacy)","description":"Stub endpoint kept for backwards compatibility (legacyUrl: /createPurchaseRequest). Always returns a fixed PR-LEGACY id.","parameters":[{"$ref":"#/components/parameters/erp.InstanceId"}],"requestBody":{"description":"Purchase request payload (ignored by stub)","content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Submitted","content":{"application/json":{"schema":{"type":"object","properties":{"purchase_request":{"type":"object","properties":{"id":{"type":"string","example":"PR-LEGACY"},"status":{"type":"string","example":"Submitted"}}}}}}}}},"tags":["erp"]}},"/purple-suite/erp/purchase-requests/pending":{"get":{"summary":"List pending purchase requests (legacy)","description":"Stub endpoint kept for backwards compatibility (legacyUrl: /getPendingPurchases). Always returns an empty array.","parameters":[{"$ref":"#/components/parameters/erp.InstanceId"}],"responses":{"200":{"description":"Empty list stub","content":{"application/json":{"schema":{"type":"array","items":{}}}}}},"tags":["erp"]}},"/purple-suite/erp/purchase-requests/history":{"get":{"summary":"List historical purchase requests (legacy)","description":"Stub endpoint kept for backwards compatibility (legacyUrl: /getHistoricalPurchases). Public (no auth required). Always returns an empty array.","parameters":[],"responses":{"200":{"description":"Empty list stub","content":{"application/json":{"schema":{"type":"array","items":{}}}}}},"security":[],"tags":["erp"]}},"/purple-suite/erp/approvals":{"post":{"summary":"Create approval (legacy)","description":"Stub endpoint kept for backwards compatibility (legacyUrl: /createApproval). Always returns APV-LEGACY id.","parameters":[{"$ref":"#/components/parameters/erp.InstanceId"}],"requestBody":{"description":"Approval payload (ignored by stub)","content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Created","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"data":{"type":"object","properties":{"id":{"type":"string","example":"APV-LEGACY"}}}}}}}}},"tags":["erp"]},"get":{"x-odata":false,"summary":"List approvals (legacy)","description":"Stub endpoint kept for backwards compatibility (legacyUrl: /getApprovals). Always returns an empty data array.","parameters":[{"$ref":"#/components/parameters/erp.InstanceId"}],"responses":{"200":{"description":"Empty list stub","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"array","items":{}}}}}}}},"tags":["erp"]}},"/purple-suite/erp/approvals/{approvalId}":{"patch":{"summary":"Update approval (legacy)","description":"Stub endpoint kept for backwards compatibility (legacyUrl: /updateApproval). Always returns an empty data object.","parameters":[{"$ref":"#/components/parameters/erp.InstanceId"},{"in":"path","name":"approvalId","required":true,"schema":{"type":"string"},"description":"Approval ID","example":"APV-0001"}],"requestBody":{"description":"Approval update payload (ignored by stub)","content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"object"}}}}}}},"tags":["erp"]}},"/purple-suite/expense/trips":{"get":{"summary":"List business trips","description":"A trip is the travel-request container in Purple Expense: traveler, dates, origin/destination, purpose, cost centre and approval state. Bookings hang off a trip via trip_id and an expense report can link to one via trip_id. Use this to find a traveler's upcoming or past travel; use bookings for the individual flight/hotel reservations. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, … because $ is not a legal tool-input key).","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/expense.Trip"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["expense"]},"post":{"summary":"Create a business trip","description":"Opens a new trip in `planned` status for a traveler. Create the trip first when you want to group bookings and expenses under one itinerary — note that booking a flight or hotel without a trip_id auto-creates one for you.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.Trip"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.Trip"}}}}},"tags":["expense"]}},"/purple-suite/expense/trips/{id}":{"get":{"summary":"Get a business trip by id","description":"Returns one trip with its itinerary, cost fields and approval stamps (approved_by/at). Bookings and expense items are separate records that reference this trip's id.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.Trip"}}}},"404":{"description":"Not found"}},"tags":["expense"]},"patch":{"summary":"Edit a business trip","description":"Partial update of trip details (dates, destination, purpose, cost centre, estimated cost). For the planned → approved transition use the dedicated approve action instead of setting `status` here.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.Trip"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.Trip"}}}},"404":{"description":"Not found"}},"tags":["expense"]},"delete":{"summary":"Delete a business trip","description":"DESTRUCTIVE and irreversible: removes the trip record. Bookings and expense items that referenced it are left pointing at a missing trip. Prefer setting status to `cancelled` unless the user really wants it gone.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["expense"]}},"/purple-suite/expense/trips/{id}/approve":{"post":{"summary":"Pre-approve a trip","description":"Manager pre-approval of the TRIP itself (the travel request), not of any spend. Transitions a trip from planned to approved and stamps approved_by/approved_at. Cannot approve a trip that is already approved, in_progress, completed or cancelled. Reimbursement of the money is a separate flow on expense reports.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"approved_by":{"type":"string","example":"manager@acme.com"},"approval_notes":{"type":"string","example":"Within policy. Approved."}},"required":["approved_by"]}}}},"responses":{"200":{"description":"Trip approved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.Trip"}}}},"404":{"description":"Trip not found"},"422":{"description":"Trip cannot be approved in its current state"}},"tags":["expense"]}},"/purple-suite/expense/bookings":{"get":{"summary":"List travel bookings (reservations)","description":"A booking is one confirmed reservation — flight, hotel, car or rail — with vendor, confirmation number, cost and start/end datetimes, usually attached to a trip via trip_id and, when made from inventory, to the source flight_id/hotel_id. Bookings are what the traveler actually holds; flights and hotels are the bookable catalogue. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, … because $ is not a legal tool-input key).","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/expense.Booking"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["expense"]},"post":{"summary":"Record a travel booking manually","description":"Creates a booking row for a reservation made outside the inventory (an existing confirmation number, a car or rail segment). It does NOT touch flight/hotel availability — to book from the catalogue and decrement seats/rooms, use the flight or hotel book action instead.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.Booking"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.Booking"}}}}},"tags":["expense"]}},"/purple-suite/expense/bookings/{id}":{"get":{"summary":"Get a travel booking by id","description":"Returns one reservation with its vendor, confirmation number, cost, cabin/room class and, if cancelled, the cancellation stamps and refund amount.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.Booking"}}}},"404":{"description":"Not found"}},"tags":["expense"]},"patch":{"summary":"Edit a travel booking","description":"Partial update of reservation details (times, vendor, confirmation number, cost, traveler). To cancel, use the dedicated cancel action rather than setting `status` here — cancel is what writes the cancellation stamps and refund.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.Booking"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.Booking"}}}},"404":{"description":"Not found"}},"tags":["expense"]},"delete":{"summary":"Delete a travel booking","description":"DESTRUCTIVE and irreversible: erases the reservation record entirely, with no refund and no release of inventory. Use the cancel action instead for a normal traveler-facing cancellation.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["expense"]}},"/purple-suite/expense/bookings/{id}/cancel":{"post":{"summary":"Cancel a booking","description":"The traveler-facing way to cancel a reservation: marks the booking cancelled and records who cancelled it, why, and the refund amount. Cannot cancel a booking that is already cancelled or completed. Prefer this over deleting the booking, which erases the record and issues no refund.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"cancelled_by":{"type":"string","example":"alice@acme.com"},"cancellation_reason":{"type":"string","example":"Trip rescheduled"},"refund_amount":{"type":"number","example":434}},"required":["cancelled_by"]}}}},"responses":{"200":{"description":"Booking cancelled","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.Booking"}}}},"404":{"description":"Booking not found"},"422":{"description":"Booking cannot be cancelled in its current state"}},"tags":["expense"]}},"/purple-suite/expense/expense_reports":{"get":{"summary":"List expense reports","description":"An expense report is the reimbursement envelope an employee submits: a named batch of expense items with an employee_email, optional trip_id, cost centre, approver and a status of draft → submitted → approved/rejected → reimbursed. `total_amount` is server-maintained from the attached items. Use this to answer 'what have I claimed / what is awaiting my approval'; use expense items for individual receipts. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, … because $ is not a legal tool-input key).","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/expense.ExpenseReport"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["expense"]},"post":{"summary":"Create a draft expense report","description":"Opens a new report in `draft` for an employee, optionally linked to a trip. Add expense items to it (they carry report_id) and then call the submit action — do not set total_amount, it is computed.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.ExpenseReport"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.ExpenseReport"}}}}},"tags":["expense"]}},"/purple-suite/expense/expense_reports/{id}":{"get":{"summary":"Get an expense report by id","description":"Returns one report with its status, server-computed total and the full audit stamps for each transition (submitted_by/at, approved_by/at, rejection_reason, reimbursed_at, reimbursement_reference).","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.ExpenseReport"}}}},"404":{"description":"Not found"}},"tags":["expense"]},"patch":{"summary":"Edit an expense report's details","description":"Partial update of report metadata (name, cost centre, project code, approver). Use the dedicated submit / approve / reject / reimburse actions to move `status` — they enforce the state machine and write the audit stamps. `total_amount` is server-maintained and ignored on input.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.ExpenseReport"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.ExpenseReport"}}}},"404":{"description":"Not found"}},"tags":["expense"]},"delete":{"summary":"Delete an expense report","description":"DESTRUCTIVE and irreversible: removes the report, including any approval and reimbursement history. Expense items that referenced it are orphaned. Confirm with the user before calling.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["expense"]}},"/purple-suite/expense/expense_reports/{id}/submit":{"post":{"summary":"Submit a draft expense report for approval","description":"Step 1 of the reimbursement chain (submit → approve → reimburse). The EMPLOYEE sends their draft report to its approver: transitions draft → submitted and stamps submitted_by/at. Only draft reports can be submitted; use approve for the manager's decision.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"submitted_by":{"type":"string","example":"alice@acme.com","description":"Defaults to employee_email if omitted"}}}}}},"responses":{"200":{"description":"Report submitted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.ExpenseReport"}}}},"404":{"description":"Report not found"},"422":{"description":"Report is not in draft status"}},"tags":["expense"]}},"/purple-suite/expense/expense_reports/{id}/approve":{"post":{"summary":"Approve a submitted expense report","description":"Step 2 of the reimbursement chain: the APPROVER signs off on a submitted report, transitioning submitted → approved with approved_by/at and optional approver_notes. Approving does not pay the employee — call the reimburse action for that. Only submitted reports can be approved; use reject to decline one.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"approved_by":{"type":"string","example":"manager@acme.com"},"approver_notes":{"type":"string","example":"Within policy. Approved."}},"required":["approved_by"]}}}},"responses":{"200":{"description":"Report approved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.ExpenseReport"}}}},"404":{"description":"Report not found"},"422":{"description":"Report is not in submitted status"}},"tags":["expense"]}},"/purple-suite/expense/expense_reports/{id}/reject":{"post":{"summary":"Reject a submitted expense report","description":"The approver's decline decision, the counterpart of approve: transitions submitted → rejected with rejected_by/at. `rejection_reason` is REQUIRED and is shown back to the employee. Only submitted reports can be rejected.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"rejected_by":{"type":"string","example":"manager@acme.com"},"rejection_reason":{"type":"string","example":"Receipt missing for >$75 item"}},"required":["rejected_by","rejection_reason"]}}}},"responses":{"200":{"description":"Report rejected","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.ExpenseReport"}}}},"404":{"description":"Report not found"},"422":{"description":"Report is not in submitted status, or rejection_reason is missing"}},"tags":["expense"]}},"/purple-suite/expense/expense_reports/{id}/reimburse":{"post":{"summary":"Mark an approved expense report as paid out","description":"Final step of the reimbursement chain, normally performed by finance: transitions approved → reimbursed and records reimbursed_at, reimbursement_method (defaults to direct_deposit) and a payment reference. This is the money-moving step and is terminal — only approved reports qualify, and approve alone does not pay.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"reimbursement_method":{"type":"string","enum":["direct_deposit","check","wire"],"example":"direct_deposit"},"reimbursement_reference":{"type":"string","example":"REIM-AB12CD"}}}}}},"responses":{"200":{"description":"Report reimbursed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.ExpenseReport"}}}},"404":{"description":"Report not found"},"422":{"description":"Report is not in approved status"}},"tags":["expense"]}},"/purple-suite/expense/expense_items":{"get":{"summary":"List expense line items (receipts)","description":"An expense item is a single spend line — one receipt: merchant, category, transaction_date, amount (plus FX to base currency), payment method, receipt_url and a status that can be flagged_policy with a policy_violation note. Items attach to an expense report via report_id and optionally a trip via trip_id. Use this for individual charges; use expense reports for the submitted envelope. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, … because $ is not a legal tool-input key).","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/expense.ExpenseItem"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["expense"]},"post":{"summary":"Add an expense line item to a report","description":"Records one charge against the report named in `report_id` (and optionally a trip). Creating an item recomputes the parent report's total_amount. Categories and limits are governed by the expense_policies collection.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.ExpenseItem"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.ExpenseItem"}}}}},"tags":["expense"]}},"/purple-suite/expense/expense_items/{id}":{"get":{"summary":"Get an expense line item by id","description":"Returns one spend line with its amount, currency conversion, receipt link, billability and any policy violation recorded against it.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.ExpenseItem"}}}},"404":{"description":"Not found"}},"tags":["expense"]},"patch":{"summary":"Edit an expense line item","description":"Partial update of one charge (amount, category, merchant, receipt_url, billability). Changing the amount recomputes the parent report's total_amount.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.ExpenseItem"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.ExpenseItem"}}}},"404":{"description":"Not found"}},"tags":["expense"]},"delete":{"summary":"Remove an expense line item from a report","description":"DESTRUCTIVE and irreversible: deletes the receipt line and recomputes the parent report's total_amount downward. There is no undo.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["expense"]}},"/purple-suite/expense/expense_policies":{"get":{"summary":"List spend policy rules by expense category","description":"An expense policy is the SPEND rule for one category (meals, lodging, mileage, …): daily_limit, per_transaction_limit, the receipt threshold, the pre-approval threshold, allowed payment methods and the mileage rate/unit. Read these to answer 'is this claim within policy' or 'what is the mileage rate'. Distinct from travel_policies, which govern flight cabin eligibility, and per_diem_rates, which are destination allowances. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, … because $ is not a legal tool-input key).","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/expense.ExpensePolicy"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["expense"]},"post":{"summary":"Create a spend policy rule for a category","description":"Defines the limits and receipt/pre-approval thresholds for one expense category. Administrative configuration — it changes what counts as a violation for future claims.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.ExpensePolicy"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.ExpensePolicy"}}}}},"tags":["expense"]}},"/purple-suite/expense/expense_policies/{id}":{"get":{"summary":"Get a spend policy rule by id","description":"Returns one category's limits, thresholds, allowed payment methods and effective date.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.ExpensePolicy"}}}},"404":{"description":"Not found"}},"tags":["expense"]},"patch":{"summary":"Edit a spend policy rule","description":"Partial update of a category's limits or thresholds. Administrative change that alters what is flagged as a policy violation going forward; it does not re-evaluate existing expense items.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.ExpensePolicy"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.ExpensePolicy"}}}},"404":{"description":"Not found"}},"tags":["expense"]},"delete":{"summary":"Delete a spend policy rule","description":"DESTRUCTIVE and irreversible: removes the category's limits entirely, so that category becomes unconstrained. Confirm with the user before calling.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["expense"]}},"/purple-suite/expense/approvals":{"get":{"summary":"List approval requests (trips and expense reports)","description":"An approval is the routing/audit record for one decision: target_type (trip or expense_report) plus target_id, the requester, the assigned approver, amount, status and decision notes. Query it to build an approver's queue across both trips and reports. The decision itself is made with the trip approve or expense-report approve/reject actions, not by editing this record. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, … because $ is not a legal tool-input key).","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/expense.Approval"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["expense"]},"post":{"summary":"Raise an approval request","description":"Creates a routing record asking a named approver to decide on a trip or expense report. It records the request only; it does not itself change the target record's status.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.Approval"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.Approval"}}}}},"tags":["expense"]}},"/purple-suite/expense/approvals/{id}":{"get":{"summary":"Get an approval request by id","description":"Returns one approval record — what it targets, who must decide, the amount, and the decision and notes if made.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.Approval"}}}},"404":{"description":"Not found"}},"tags":["expense"]},"patch":{"summary":"Edit an approval request","description":"Partial update of the routing record (reassign the approver, note a decision). Prefer the trip approve or expense-report approve/reject/reimburse actions to actually move the target through its state machine.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.Approval"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.Approval"}}}},"404":{"description":"Not found"}},"tags":["expense"]},"delete":{"summary":"Delete an approval request","description":"DESTRUCTIVE and irreversible: removes the approval record and its decision history. The target trip or report is left unchanged.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["expense"]}},"/purple-suite/expense/airlines":{"get":{"summary":"List airline carriers (reference data)","description":"An airline is reference data about a carrier — IATA code, name, alliance, hub airports and historical on-time rate. Use it to resolve or describe the airline_code on a flight; it holds no schedules or availability, which live on flights. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, … because $ is not a legal tool-input key).","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/expense.Airline"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["expense"]},"post":{"summary":"Add an airline carrier","description":"Adds a carrier to the reference list (code, name, alliance, hubs). Seed/admin data — it does not create any bookable flights.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.Airline"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.Airline"}}}}},"tags":["expense"]}},"/purple-suite/expense/airlines/{id}":{"get":{"summary":"Get an airline carrier by id","description":"Returns one carrier's reference details: IATA code, name, alliance, hubs and on-time rate.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.Airline"}}}},"404":{"description":"Not found"}},"tags":["expense"]},"patch":{"summary":"Edit an airline carrier","description":"Partial update of carrier reference data. Does not affect flights already referencing the carrier code.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.Airline"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.Airline"}}}},"404":{"description":"Not found"}},"tags":["expense"]},"delete":{"summary":"Delete an airline carrier","description":"DESTRUCTIVE and irreversible: removes the carrier from reference data. Flights keep their airline_code but can no longer be resolved to a carrier.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["expense"]}},"/purple-suite/expense/flights/search":{"get":{"x-odata":false,"summary":"Search bookable flights","description":"The shopping entry point for air travel — prefer this over listing flights. Filters the inventory by route, date, cabin and price, returns only flights with at least one matching fare that still has seats available, cheapest fare first. Read-only: pass a returned flight's id to the flight book action to actually reserve a seat.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"query","name":"origin","schema":{"type":"string"},"example":"JFK","description":"Departure airport code"},{"in":"query","name":"destination","schema":{"type":"string"},"example":"SFO","description":"Arrival airport code"},{"in":"query","name":"date","schema":{"type":"string","format":"date"},"example":"2026-06-10","description":"Departure date (YYYY-MM-DD)"},{"in":"query","name":"cabin","schema":{"type":"string","enum":["Economy","Premium Economy","Business","First"]},"description":"Only return flights offering this cabin with seats available"},{"in":"query","name":"max_price","schema":{"type":"number"},"description":"Only return flights with a matching fare at or below this price"},{"in":"query","name":"limit","schema":{"type":"integer","default":20}}],"responses":{"200":{"description":"Matching flights","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/expense.Flight"}},"total":{"type":"integer"}}}}}}},"tags":["expense"]}},"/purple-suite/expense/flights":{"get":{"summary":"List the raw flight inventory","description":"A flight is one scheduled segment in the bookable catalogue: number, carrier, origin/destination, times, stops, status and a `fares` array of cabins with price and seats_available. This is the unfiltered catalogue dump — to shop for travel prefer the flight search operation, which filters by route/date/cabin/price and returns only flights with seats left, cheapest first. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, … because $ is not a legal tool-input key).","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/expense.Flight"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["expense"]},"post":{"summary":"Add a flight to the inventory","description":"Seeds a new bookable segment with its cabins/fares. Inventory administration — this is not how a traveler books; use the flight book action.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.Flight"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.Flight"}}}}},"tags":["expense"]}},"/purple-suite/expense/flights/{id}":{"get":{"summary":"Get a flight by id","description":"Returns one segment with its schedule and live per-cabin fares/seats_available. Use before booking to confirm the cabin and price.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.Flight"}}}},"404":{"description":"Not found"}},"tags":["expense"]},"patch":{"summary":"Edit a flight in the inventory","description":"Partial update of a segment's schedule, status or fares. Inventory administration — booking already decrements seats_available for you.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.Flight"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.Flight"}}}},"404":{"description":"Not found"}},"tags":["expense"]},"delete":{"summary":"Delete a flight from the inventory","description":"DESTRUCTIVE and irreversible: removes the segment from the catalogue. Existing bookings keep their flight_id but it no longer resolves.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["expense"]}},"/purple-suite/expense/flights/{id}/book":{"post":{"summary":"Book a flight","description":"Reserves a seat in the given cabin and creates a confirmed booking that links back to this flight via flight_id. Decrements the cabin's seats_available. Fails if the cabin is sold out or the flight is cancelled. Premium cabins are subject to fare-class policy (see the travel_policies collection): if the cabin's min_seniority exceeds the traveler's level, the booking is rejected with 403. If no trip_id is supplied, a trip is auto-created from the flight's route/dates and the booking is attached to it.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"traveler_email":{"type":"string","example":"alice@acme.com"},"cabin":{"type":"string","enum":["Economy","Premium Economy","Business","First"],"example":"Economy","description":"Defaults to Economy"},"trip_id":{"type":"string","example":"TRIP-0001","description":"Optional. Trip to attach the booking to; if omitted, a new trip is auto-created from the flight."},"confirmation_number":{"type":"string","description":"Optional; auto-generated if omitted"}},"required":["traveler_email"]}}}},"responses":{"201":{"description":"Flight booked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.Booking"}}}},"403":{"description":"Traveler is not eligible for the requested cabin under fare-class policy"},"404":{"description":"Flight not found"},"422":{"description":"Cabin sold out, flight cancelled, or traveler_email missing"}},"tags":["expense"]}},"/purple-suite/expense/hotels/search":{"get":{"x-odata":false,"summary":"Search bookable hotels","description":"The shopping entry point for lodging — prefer this over listing hotels. Filters the inventory by city, nightly rate and star rating, returns only active properties with rooms available, cheapest first. Read-only: pass a returned hotel's id to the hotel book action to actually reserve a room.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"query","name":"city","schema":{"type":"string"},"example":"SFO","description":"Matches city name, city_code, or address (case-insensitive substring)"},{"in":"query","name":"max_nightly_rate","schema":{"type":"number"},"description":"Only hotels at or below this nightly rate"},{"in":"query","name":"min_star_rating","schema":{"type":"number"},"description":"Only hotels at or above this star rating"},{"in":"query","name":"limit","schema":{"type":"integer","default":20}}],"responses":{"200":{"description":"Matching hotels","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/expense.Hotel"}},"total":{"type":"integer"}}}}}}},"tags":["expense"]}},"/purple-suite/expense/hotels":{"get":{"summary":"List the raw hotel inventory","description":"A hotel is one bookable property: name, brand, city/city_code, star and guest ratings, nightly_rate, amenities, room_types and rooms_available. This is the unfiltered catalogue dump — to shop for lodging prefer the hotel search operation, which filters by city, rate and star rating and returns only active properties with rooms left. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, … because $ is not a legal tool-input key).","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/expense.Hotel"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["expense"]},"post":{"summary":"Add a hotel to the inventory","description":"Seeds a new bookable property with its rates and room types. Inventory administration — travelers book via the hotel book action.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.Hotel"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.Hotel"}}}}},"tags":["expense"]}},"/purple-suite/expense/hotels/{id}":{"get":{"summary":"Get a hotel by id","description":"Returns one property with its rates, amenities, room tiers and current rooms_available. Use before booking to confirm rate and availability.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.Hotel"}}}},"404":{"description":"Not found"}},"tags":["expense"]},"patch":{"summary":"Edit a hotel in the inventory","description":"Partial update of a property's rate, status, amenities or room types. Inventory administration — booking already decrements rooms_available.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.Hotel"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.Hotel"}}}},"404":{"description":"Not found"}},"tags":["expense"]},"delete":{"summary":"Delete a hotel from the inventory","description":"DESTRUCTIVE and irreversible: removes the property from the catalogue. Existing bookings keep their hotel_id but it no longer resolves.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["expense"]}},"/purple-suite/expense/hotels/{id}/book":{"post":{"summary":"Book a hotel stay","description":"Reserves a room for the given dates and creates a confirmed booking that links back to this hotel via hotel_id. Cost is the nightly rate × nights. Decrements rooms_available. If no trip_id is supplied, a trip is auto-created from the hotel's city/dates and the booking is attached to it.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"traveler_email":{"type":"string","example":"alice@acme.com"},"check_in":{"type":"string","format":"date","example":"2026-06-10"},"check_out":{"type":"string","format":"date","example":"2026-06-13"},"room_type":{"type":"string","example":"Standard King","description":"Optional; uses the matching room_types rate, else the base nightly_rate"},"trip_id":{"type":"string","example":"TRIP-0001","description":"Optional. Trip to attach the booking to; if omitted, a new trip is auto-created from the hotel stay."},"confirmation_number":{"type":"string","description":"Optional; auto-generated if omitted"}},"required":["traveler_email","check_in","check_out"]}}}},"responses":{"201":{"description":"Hotel booked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.Booking"}}}},"404":{"description":"Hotel not found"},"422":{"description":"No rooms available, invalid dates, or required fields missing"}},"tags":["expense"]}},"/purple-suite/expense/travel_policies":{"get":{"summary":"List fare-class (cabin) eligibility rules","description":"A travel policy is a cabin-eligibility rule: the minimum seniority a traveler must hold to book a given cabin_class. It is enforced at booking time — the flight book action rejects an under-ranked traveler with 403. Read it to answer 'am I allowed to fly business'. Distinct from expense_policies (spend limits per category). Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, … because $ is not a legal tool-input key).","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/expense.TravelPolicy"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["expense"]},"post":{"summary":"Create a fare-class eligibility rule","description":"Sets the minimum seniority required for a cabin class. Administrative — it takes effect on future flight bookings.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.TravelPolicy"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.TravelPolicy"}}}}},"tags":["expense"]}},"/purple-suite/expense/travel_policies/{id}":{"get":{"summary":"Get a fare-class eligibility rule by id","description":"Returns one cabin rule: the cabin_class it governs, its min_seniority and effective date.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.TravelPolicy"}}}},"404":{"description":"Not found"}},"tags":["expense"]},"patch":{"summary":"Edit a fare-class eligibility rule","description":"Partial update of a cabin's min_seniority or description. Changes who can book that cabin from now on; existing bookings are untouched.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.TravelPolicy"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.TravelPolicy"}}}},"404":{"description":"Not found"}},"tags":["expense"]},"delete":{"summary":"Delete a fare-class eligibility rule","description":"DESTRUCTIVE and irreversible: removes the restriction, leaving that cabin bookable by anyone. Confirm with the user before calling.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["expense"]}},"/purple-suite/expense/per_diem_rates/calculate":{"get":{"x-odata":false,"summary":"Calculate per diem for a destination","description":"Looks up a city's per diem rate (by city_code or city name) and totals it over a number of days, returning lodging, M&IE and combined amounts. Use this whenever the question is 'how much allowance for N days in X'; use the per diem rates list only to browse the underlying rate table. Read-only — it creates nothing.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"query","name":"city_code","schema":{"type":"string"},"example":"SFO","description":"Destination airport/city code (preferred). Provide this or city."},{"in":"query","name":"city","schema":{"type":"string"},"example":"San Francisco","description":"Destination city name (case-insensitive substring). Used if city_code is omitted."},{"in":"query","name":"days","schema":{"type":"integer","default":1},"example":3,"description":"Number of days to total the allowance over"}],"responses":{"200":{"description":"Per diem totals for the destination","content":{"application/json":{"schema":{"type":"object","properties":{"city":{"type":"string","example":"San Francisco"},"city_code":{"type":"string","example":"SFO"},"country":{"type":"string","example":"US"},"currency":{"type":"string","example":"USD"},"days":{"type":"integer","example":3},"lodging_rate":{"type":"number","example":325},"mie_rate":{"type":"number","example":79},"total_daily_rate":{"type":"number","example":404},"lodging_total":{"type":"number","example":975},"mie_total":{"type":"number","example":237},"total":{"type":"number","example":1212}}}}}},"404":{"description":"No per diem rate found for the destination"},"422":{"description":"Missing city_code/city, or days is not a positive integer"}},"tags":["expense"]}},"/purple-suite/expense/per_diem_rates":{"get":{"summary":"List per diem allowance rates by destination","description":"A per diem rate is a GSA-style daily travel allowance for a city: lodging_rate plus mie_rate (meals & incidentals) and their total, in the instance's base currency. Use this to browse or compare destinations; to total an allowance over a stay use the per diem calculate operation instead. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, … because $ is not a legal tool-input key).","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/expense.PerDiemRate"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["expense"]},"post":{"summary":"Create a per diem rate for a destination","description":"Adds a city's lodging and M&IE allowance. Administrative rate-table maintenance, not a traveler action.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.PerDiemRate"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.PerDiemRate"}}}}},"tags":["expense"]}},"/purple-suite/expense/per_diem_rates/{id}":{"get":{"summary":"Get a per diem rate by id","description":"Returns one destination's lodging, M&IE and combined daily allowance with its effective date.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.PerDiemRate"}}}},"404":{"description":"Not found"}},"tags":["expense"]},"patch":{"summary":"Edit a per diem rate","description":"Partial update of a destination's lodging/M&IE allowance. Affects future per diem calculations.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.PerDiemRate"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/expense.PerDiemRate"}}}},"404":{"description":"Not found"}},"tags":["expense"]},"delete":{"summary":"Delete a per diem rate","description":"DESTRUCTIVE and irreversible: removes the destination's allowance, after which per diem calculation for that city will not resolve.","parameters":[{"$ref":"#/components/parameters/expense.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["expense"]}},"/purple-suite/grid/bases":{"get":{"summary":"List Grid bases","description":"A base is a Grid workspace (Airtable-style) that owns a set of tables; it sits at the top of the bases → tables → fields/records/views hierarchy, so start here when you don't yet have a baseId. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, … because $ is not a legal tool-input key).","parameters":[{"$ref":"#/components/parameters/grid.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/grid.Base"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["grid"]},"post":{"summary":"Create a Grid base","description":"Creates an empty base (workspace) that tables can then be added to. Only `name` is meaningful on input — id, tableCount and timestamps are server-assigned.","parameters":[{"$ref":"#/components/parameters/grid.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/grid.Base"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/grid.Base"}}}}},"tags":["grid"]}},"/purple-suite/grid/bases/{id}":{"get":{"summary":"Get a Grid base by id","description":"Returns one base's metadata (name, tableCount, creator). Use when you already have a base id; use the list operation to find one by name.","parameters":[{"$ref":"#/components/parameters/grid.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/grid.Base"}}}},"404":{"description":"Not found"}},"tags":["grid"]},"patch":{"summary":"Rename or edit a Grid base","description":"Partial update of base-level metadata such as `name`. Does not change the tables or records inside the base.","parameters":[{"$ref":"#/components/parameters/grid.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/grid.Base"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/grid.Base"}}}},"404":{"description":"Not found"}},"tags":["grid"]},"delete":{"summary":"Delete a Grid base","description":"DESTRUCTIVE and irreversible: removes the base record. Its tables/records are not cleaned up, so they are orphaned rather than restored. Confirm with the user before calling.","parameters":[{"$ref":"#/components/parameters/grid.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["grid"]}},"/purple-suite/grid/tables":{"get":{"summary":"List Grid tables","description":"A table is a spreadsheet-like grid of records inside one base, and owns its own fields (columns) and views. Pass `baseId` to scope the list to a single base — that is the normal second step after listing bases. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, … because $ is not a legal tool-input key).","parameters":[{"$ref":"#/components/parameters/grid.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."},{"in":"query","name":"baseId","schema":{"type":"string"},"description":"Filter tables by parent base ID"}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/grid.Table"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["grid"]},"post":{"summary":"Create a Grid table","description":"Adds a table to an existing base. Requires `name` and the parent `baseId`; fieldCount/recordCount are maintained by the server. Create the table before creating its fields or records.","parameters":[{"$ref":"#/components/parameters/grid.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/grid.Table"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/grid.Table"}}}}},"tags":["grid"]}},"/purple-suite/grid/tables/{id}":{"get":{"summary":"Get a Grid table by id","description":"Returns one table's metadata (name, parent baseId, field/record counts). Does NOT return the table's rows — use the records list filtered by `tableId` for data.","parameters":[{"$ref":"#/components/parameters/grid.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/grid.Table"}}}},"404":{"description":"Not found"}},"tags":["grid"]},"patch":{"summary":"Rename or edit a Grid table","description":"Partial update of table metadata such as `name` or the parent `baseId`. Does not alter rows or columns.","parameters":[{"$ref":"#/components/parameters/grid.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/grid.Table"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/grid.Table"}}}},"404":{"description":"Not found"}},"tags":["grid"]},"delete":{"summary":"Delete a Grid table","description":"DESTRUCTIVE and irreversible: removes the table definition. Its fields, records and views are left orphaned, not restored. Confirm with the user before calling.","parameters":[{"$ref":"#/components/parameters/grid.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["grid"]}},"/purple-suite/grid/fields":{"get":{"summary":"List Grid field (column) definitions","description":"A field is a column definition on a table — its name, its type (singleSelect, date, formula, rollup, …) and type-specific `options` such as select choices. Fields describe the SHAPE of a table; the cell values live on records. Pass `tableId` to get one table's columns, e.g. before writing a record. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, … because $ is not a legal tool-input key).","parameters":[{"$ref":"#/components/parameters/grid.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."},{"in":"query","name":"tableId","schema":{"type":"string"},"description":"Filter fields by parent table ID"}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/grid.Field"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["grid"]},"post":{"summary":"Add a column to a Grid table","description":"Defines a new field (column) on the table given by `tableId`: `name`, `type`, and for choice/lookup types an `options` object. Adding a field does not backfill values on existing records.","parameters":[{"$ref":"#/components/parameters/grid.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/grid.Field"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/grid.Field"}}}}},"tags":["grid"]}},"/purple-suite/grid/fields/{id}":{"get":{"summary":"Get a Grid field definition by id","description":"Returns one column definition (name, type, options). Use it to learn the legal values of a singleSelect before setting that cell on a record.","parameters":[{"$ref":"#/components/parameters/grid.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/grid.Field"}}}},"404":{"description":"Not found"}},"tags":["grid"]},"patch":{"summary":"Edit a Grid field definition","description":"Partial update of a column's name, type or `options` (e.g. adding a select choice). Changing `type` does not convert values already stored on records.","parameters":[{"$ref":"#/components/parameters/grid.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/grid.Field"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/grid.Field"}}}},"404":{"description":"Not found"}},"tags":["grid"]},"delete":{"summary":"Delete a Grid field definition","description":"DESTRUCTIVE and irreversible: removes the column definition. Values previously written under that field name stay on the records but become undescribed. Confirm with the user before calling.","parameters":[{"$ref":"#/components/parameters/grid.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["grid"]}},"/purple-suite/grid/records":{"get":{"summary":"List Grid records (table rows)","description":"A record is one row of user data in a Grid table: the built-in name/status/assignee columns plus a free-form `fields` map of cell values keyed by field name. This is the actual content of the Grid app — use fields for column definitions and views for saved layouts. Pass `tableId` to scope to one table. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, … because $ is not a legal tool-input key).","parameters":[{"$ref":"#/components/parameters/grid.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."},{"in":"query","name":"tableId","schema":{"type":"string"},"description":"Filter records by parent table ID"}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/grid.Record"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["grid"]},"post":{"summary":"Create a Grid record (row)","description":"Inserts a row into the table given by `tableId`. Set the built-in name/status/assignee columns directly and any custom columns inside `fields`, keyed by the field name from the fields collection.","parameters":[{"$ref":"#/components/parameters/grid.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/grid.Record"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/grid.Record"}}}}},"tags":["grid"]}},"/purple-suite/grid/records/{id}":{"get":{"summary":"Get a Grid record (row) by id","description":"Returns one row with all of its cell values. Use the records list with `tableId` when you don't have the row id.","parameters":[{"$ref":"#/components/parameters/grid.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/grid.Record"}}}},"404":{"description":"Not found"}},"tags":["grid"]},"patch":{"summary":"Edit cell values on a Grid record","description":"Partial update of one row — the normal way to change a cell (e.g. move a row's `status`, reassign it, or set a custom column inside `fields`). Only the keys you send are changed.","parameters":[{"$ref":"#/components/parameters/grid.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/grid.Record"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/grid.Record"}}}},"404":{"description":"Not found"}},"tags":["grid"]},"delete":{"summary":"Delete a Grid record (row)","description":"DESTRUCTIVE and irreversible: permanently removes the row and its cell values. There is no trash or undo. Confirm with the user before calling.","parameters":[{"$ref":"#/components/parameters/grid.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["grid"]}},"/purple-suite/grid/views":{"get":{"summary":"List Grid saved views","description":"A view is a saved presentation of one table — its layout (grid, kanban, calendar, gantt, gallery, form), its `filterByFormula` and its sort. Views hold no data of their own; read records for rows. Pass `tableId` to list one table's views. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, … because $ is not a legal tool-input key).","parameters":[{"$ref":"#/components/parameters/grid.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."},{"in":"query","name":"tableId","schema":{"type":"string"},"description":"Filter views by parent table ID"}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/grid.View"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["grid"]},"post":{"summary":"Create a Grid saved view","description":"Saves a new layout over the table given by `tableId`: a `type` (grid/kanban/calendar/…), an optional `filterByFormula` and a sort field/direction. Creating a view never changes the underlying rows.","parameters":[{"$ref":"#/components/parameters/grid.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/grid.View"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/grid.View"}}}}},"tags":["grid"]}},"/purple-suite/grid/views/{id}":{"get":{"summary":"Get a Grid saved view by id","description":"Returns one view's configuration (type, filter formula, sort). It does not evaluate the view or return matching rows.","parameters":[{"$ref":"#/components/parameters/grid.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/grid.View"}}}},"404":{"description":"Not found"}},"tags":["grid"]},"patch":{"summary":"Edit a Grid saved view","description":"Partial update of a view's name, layout type, `filterByFormula` or sort. Affects presentation only, never record data.","parameters":[{"$ref":"#/components/parameters/grid.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/grid.View"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/grid.View"}}}},"404":{"description":"Not found"}},"tags":["grid"]},"delete":{"summary":"Delete a Grid saved view","description":"DESTRUCTIVE and irreversible: removes the saved layout. The table's records are unaffected. Confirm with the user before calling.","parameters":[{"$ref":"#/components/parameters/grid.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["grid"]}},"/purple-suite/grid/automations":{"get":{"summary":"List automations","parameters":[{"$ref":"#/components/parameters/grid.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/grid.Automation"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["grid"]},"post":{"summary":"Create automation","parameters":[{"$ref":"#/components/parameters/grid.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/grid.Automation"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/grid.Automation"}}}}},"tags":["grid"]}},"/purple-suite/grid/automations/{id}":{"get":{"summary":"Get automation","parameters":[{"$ref":"#/components/parameters/grid.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/grid.Automation"}}}},"404":{"description":"Not found"}},"tags":["grid"]},"patch":{"summary":"Update automation","parameters":[{"$ref":"#/components/parameters/grid.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/grid.Automation"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/grid.Automation"}}}},"404":{"description":"Not found"}},"tags":["grid"]},"delete":{"summary":"Delete automation","parameters":[{"$ref":"#/components/parameters/grid.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["grid"]}},"/purple-suite/hrbp/resolve_filters":{"post":{"summary":"Resolve & validate HRBP scope filters (tp_resolve_scope)","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"}],"responses":{"200":{"description":"Resolve & validate HRBP scope filters (tp_resolve_scope)"},"400":{"description":"Bad request"}},"tags":["hrbp"]}},"/purple-suite/hrbp/available_metrics":{"get":{"summary":"List predefined talent metrics (tp_list_metrics)","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"}],"responses":{"200":{"description":"List predefined talent metrics (tp_list_metrics)"},"400":{"description":"Bad request"}},"tags":["hrbp"]}},"/purple-suite/hrbp/available_metric_categories":{"get":{"summary":"List metric categories","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"}],"responses":{"200":{"description":"List metric categories"},"400":{"description":"Bad request"}},"tags":["hrbp"]}},"/purple-suite/hrbp/execute_known_metric":{"post":{"summary":"Compute one predefined metric (tp_execute_metric)","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"}],"responses":{"200":{"description":"Compute one predefined metric (tp_execute_metric)"},"400":{"description":"Bad request"}},"tags":["hrbp"]}},"/purple-suite/hrbp/find_matching_reviews":{"post":{"summary":"Find talent reviews by filters (tp_find_reviews)","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"}],"responses":{"200":{"description":"Find talent reviews by filters (tp_find_reviews)"},"400":{"description":"Bad request"}},"tags":["hrbp"]}},"/purple-suite/hrbp/preview_review":{"post":{"summary":"Preview a talent review (computes metrics); mints idempotency_key (tp_preview_review)","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"}],"responses":{"200":{"description":"Preview a talent review (computes metrics); mints idempotency_key (tp_preview_review)"},"400":{"description":"Bad request"}},"tags":["hrbp"]}},"/purple-suite/hrbp/commit_review":{"post":{"summary":"Commit a previewed talent review (tp_commit_review)","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"}],"responses":{"200":{"description":"Commit a previewed talent review (tp_commit_review)"},"400":{"description":"Bad request"}},"tags":["hrbp"]}},"/purple-suite/hrbp/add_metric_to_review":{"post":{"summary":"Add a metric to an existing talent review (tp_add_metric)","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"}],"responses":{"200":{"description":"Add a metric to an existing talent review (tp_add_metric)"},"400":{"description":"Bad request"}},"tags":["hrbp"]}},"/purple-suite/hrbp/display_talent_review":{"post":{"summary":"Display a talent review + computed metrics (tp_display_review)","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"}],"responses":{"200":{"description":"Display a talent review + computed metrics (tp_display_review)"},"400":{"description":"Bad request"}},"tags":["hrbp"]}},"/purple-suite/hrbp/hrbp_data_analytics/query":{"post":{"summary":"HRBP data-analytics query — NL question → generated query, aggregates, inline insights, and async answer/follow-up logIds (SN hrbp_data_analytics/query). Returns 202 on submission; poll result.analysis.logId / result.followUps.logId via /hrbp_data_analytics/async_results.","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"query","name":"method","required":false,"schema":{"type":"string","enum":["fullPipeline","insightsOnly","custom","getAsyncResults"],"default":"fullPipeline"},"description":"fullPipeline needs {utterance, channelId}; insightsOnly needs {channelId} plus dataConfigurations or dataResponses; custom needs {featureFlags}; getAsyncResults needs {logId}."},{"in":"query","name":"seed","required":false,"schema":{"type":"string"},"description":"Pin the simulated model output so a run is reproducible. Omit for fresh phrasing on every call (the underlying numbers never change)."},{"in":"query","name":"readyDelayMs","required":false,"schema":{"type":"integer","default":1500,"minimum":0,"maximum":60000},"description":"How long the async answer/follow-up capabilities stay in `processing` before results land. 0 makes them readable immediately."}],"responses":{"202":{"description":"Accepted — async capabilities submitted"},"400":{"description":"Validation error (missing required field)"},"404":{"description":"getAsyncResults: unknown logId"}},"tags":["hrbp"]}},"/purple-suite/hrbp/hrbp_data_analytics/async_results":{"get":{"summary":"Batch-poll async analytics results by logId(s) (SN hrbp_data_analytics/async_results)","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"query","name":"logIds","required":false,"schema":{"type":"string"},"description":"Comma-separated log ids."},{"in":"query","name":"logId","required":false,"schema":{"type":"string"},"description":"A single log id (combinable with logIds)."}],"responses":{"200":{"description":"Batch-poll async analytics results by logId(s) (SN hrbp_data_analytics/async_results)"},"400":{"description":"Bad request"}},"tags":["hrbp"]}},"/purple-suite/hrbp/hrbp_action_recommendations_rest_api/recommendations":{"post":{"summary":"Generate action recommendations from an analytics finding — accepts {goal, content} or {goal, queryGenResponse|fullpipeline_response} (SN hrbp_action_recommendations_rest_api/recommendations)","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"}],"responses":{"200":{"description":"Generate action recommendations from an analytics finding — accepts {goal, content} or {goal, queryGenResponse|fullpipeline_response} (SN hrbp_action_recommendations_rest_api/recommendations)"},"400":{"description":"Bad request"}},"tags":["hrbp"]}},"/purple-suite/hrbp/query":{"post":{"summary":"Alias of /hrbp_data_analytics/query (pre-existing Purple path)","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"}],"responses":{"200":{"description":"Alias of /hrbp_data_analytics/query (pre-existing Purple path)"},"400":{"description":"Bad request"}},"tags":["hrbp"]}},"/purple-suite/hrbp/async_results":{"get":{"summary":"Alias of /hrbp_data_analytics/async_results (pre-existing Purple path)","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"}],"responses":{"200":{"description":"Alias of /hrbp_data_analytics/async_results (pre-existing Purple path)"},"400":{"description":"Bad request"}},"tags":["hrbp"]}},"/purple-suite/hrbp/recommendations":{"post":{"summary":"Alias of /hrbp_action_recommendations_rest_api/recommendations (pre-existing Purple path)","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"}],"responses":{"200":{"description":"Alias of /hrbp_action_recommendations_rest_api/recommendations (pre-existing Purple path)"},"400":{"description":"Bad request"}},"tags":["hrbp"]}},"/purple-suite/hrbp/fuzzy_value_resolver":{"get":{"summary":"Resolve a raw value to catalog {value, sys_id} matches via exact->startswith->contains->fuzzy->text cascade (SN FuzzyValueResolver parity)","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"query","name":"tableName","required":true,"schema":{"type":"string"}},{"in":"query","name":"columnName","required":true,"schema":{"type":"string"}},{"in":"query","name":"rawValue","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Resolve a raw value to catalog {value, sys_id} matches via exact->startswith->contains->fuzzy->text cascade (SN FuzzyValueResolver parity)"},"400":{"description":"Bad request"}},"tags":["hrbp"]}},"/purple-suite/hrbp/available_templates":{"get":{"summary":"List talent-review templates","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"}],"responses":{"200":{"description":"List talent-review templates"},"400":{"description":"Bad request"}},"tags":["hrbp"]}},"/purple-suite/hrbp/create_review_from_template":{"post":{"summary":"Create a talent review from a template (direct write)","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"}],"responses":{"200":{"description":"Create a talent review from a template (direct write)"},"400":{"description":"Bad request"}},"tags":["hrbp"]}},"/purple-suite/hrbp/search_talent_reviews":{"post":{"summary":"Search talent reviews by filter names + scope; returns templates","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"}],"responses":{"200":{"description":"Search talent reviews by filter names + scope; returns templates"},"400":{"description":"Bad request"}},"tags":["hrbp"]}},"/purple-suite/hrbp/execute_talent_review":{"post":{"summary":"Execute a talent review's stored metrics by id","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"}],"responses":{"200":{"description":"Execute a talent review's stored metrics by id"},"400":{"description":"Bad request"}},"tags":["hrbp"]}},"/purple-suite/hrbp/query_metric":{"post":{"summary":"Resolve filters + compute one metric widget","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"}],"responses":{"200":{"description":"Resolve filters + compute one metric widget"},"400":{"description":"Bad request"}},"tags":["hrbp"]}},"/purple-suite/hrbp/hr_program/summary":{"get":{"summary":"Get an HR program summary by name keyword (q)","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"}],"responses":{"200":{"description":"Get an HR program summary by name keyword (q)"},"400":{"description":"Bad request"}},"tags":["hrbp"]}},"/purple-suite/hrbp/hr_program/draft_notification":{"post":{"summary":"Raw program data + stakeholder contacts for an LLM to draft an HR program notification from (no server-built draft text)","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"}],"responses":{"200":{"description":"Raw program data + stakeholder contacts for an LLM to draft an HR program notification from (no server-built draft text)"},"400":{"description":"Bad request"}},"tags":["hrbp"]}},"/purple-suite/hrbp/hr_program/get_draft":{"post":{"summary":"Retrieve the caller's persisted notification draft HTML for a program","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"}],"responses":{"200":{"description":"Retrieve the caller's persisted notification draft HTML for a program"},"400":{"description":"Bad request"}},"tags":["hrbp"]}},"/purple-suite/hrbp/hr_program/save_draft":{"post":{"summary":"Persist the caller's notification draft HTML for a program","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"}],"responses":{"200":{"description":"Persist the caller's notification draft HTML for a program"},"400":{"description":"Bad request"}},"tags":["hrbp"]}},"/purple-suite/hrbp/hr_program/program_details/{sys_id_csv}":{"get":{"summary":"Full nested detail (fields, program type, milestones, resources, stakeholders, action plans) for one or more programs by comma-separated sys_id","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"sys_id_csv","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Full nested detail (fields, program type, milestones, resources, stakeholders, action plans) for one or more programs by comma-separated sys_id"},"400":{"description":"Bad request"}},"tags":["hrbp"]}},"/purple-suite/hrbp/hr_program":{"get":{"summary":"List the caller's stakeholder/owner programs, grouped by state with upcoming milestones","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"}],"responses":{"200":{"description":"List the caller's stakeholder/owner programs, grouped by state with upcoming milestones"},"400":{"description":"Bad request"}},"tags":["hrbp"]}},"/purple-suite/hrbp/hr_profile/new_hire":{"get":{"summary":"New hires within the calling HRBP's scope for a hire-date window (start_date/end_date, yyyy-MM-dd; defaults to the last 30 days). Returns { start_date, end_date, count, employees:[{ name, hire_date, days_since_hire, position, department, manager }] }, wrapped in {result:...} (SN HRBPNewHireDetailsSNC parity).","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"query","name":"start_date","required":false,"schema":{"type":"string"}},{"in":"query","name":"end_date","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"New hires within the calling HRBP's scope for a hire-date window (start_date/end_date, yyyy-MM-dd; defaults to the last 30 days). Returns { start_date, end_date, count, employees:[{ name, hire_date, days_since_hire, position, department, manager }] }, wrapped in {result:...} (SN HRBPNewHireDetailsSNC parity)."},"400":{"description":"Bad request"}},"tags":["hrbp"]}},"/purple-suite/hrbp/employee/{sys_id}/work_history":{"get":{"summary":"An employee's job/position history (tenure duration + current-job status), most recent first, by employee sys_id. NOT self-scoped — any internal-role caller may look up any employee. Wraps both success AND error bodies in {result:...} (its SN op routes every 200/400/500 path through response.setBody(), same precedent as /hr_profile/new_hire).","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"sys_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"An employee's job/position history (tenure duration + current-job status), most recent first, by employee sys_id. NOT self-scoped — any internal-role caller may look up any employee. Wraps both success AND error bodies in {result:...} (its SN op routes every 200/400/500 path through response.setBody(), same precedent as /hr_profile/new_hire)."},"400":{"description":"Bad request"}},"tags":["hrbp"]}},"/purple-suite/hrbp/hr_case":{"get":{"summary":"List the caller's HRBP cases (paginated, AI-enriched)","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"}],"responses":{"200":{"description":"List the caller's HRBP cases (paginated, AI-enriched)"},"400":{"description":"Bad request"}},"tags":["hrbp"]}},"/purple-suite/hrbp/hr_case/{sys_id}":{"get":{"summary":"Get full HR case detail + AI enrichment + approvers (with raw_state) + collaborators + meeting_attendees + pending-approval flag","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"sys_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Get full HR case detail + AI enrichment + approvers (with raw_state) + collaborators + meeting_attendees + pending-approval flag"},"400":{"description":"Bad request"}},"tags":["hrbp"]}},"/purple-suite/hrbp/hr_case/{sys_id}/approval":{"post":{"summary":"Approve or reject the caller's pending approval on an HR case","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"sys_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Approve or reject the caller's pending approval on an HR case"},"400":{"description":"Bad request"}},"tags":["hrbp"]}},"/purple-suite/hrbp/api/now/table/sn_hr_core_case":{"get":{"summary":"ServiceNow Table API parity: look up a case by number (sysparm_query=number=<value>); wraps in {result: [...]}","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"}],"responses":{"200":{"description":"ServiceNow Table API parity: look up a case by number (sysparm_query=number=<value>); wraps in {result: [...]}"},"400":{"description":"Bad request"}},"tags":["hrbp"]}},"/purple-suite/hrbp/api/now/table/sn_hr_core_case/{sys_id}":{"patch":{"summary":"ServiceNow Table API parity: update a case's state (+ optional work_notes) for defer(24)/resume(18)/cancel(7); wraps in {result: {...}}","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"sys_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"ServiceNow Table API parity: update a case's state (+ optional work_notes) for defer(24)/resume(18)/cancel(7); wraps in {result: {...}}"},"400":{"description":"Bad request"}},"tags":["hrbp"]}},"/purple-suite/hrbp/api/now/table/sys_user":{"get":{"summary":"ServiceNow Table API parity: sys_user lookup (sysparm_query=nameLIKE<term>) for the u_SysUser slot resolver; wraps in {result: [...]}","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"query","name":"sysparm_query","required":false,"schema":{"type":"string"},"example":"nameLIKETaco"},{"in":"query","name":"sysparm_fields","required":false,"schema":{"type":"string"},"example":"sys_id,name,email"},{"in":"query","name":"sysparm_limit","required":false,"schema":{"type":"integer"},"example":10},{"in":"query","name":"sysparm_offset","required":false,"schema":{"type":"integer"}}],"responses":{"200":{"description":"ServiceNow Table API parity: sys_user lookup (sysparm_query=nameLIKE<term>) for the u_SysUser slot resolver; wraps in {result: [...]}"},"400":{"description":"Bad request"}},"tags":["hrbp"]}},"/purple-suite/hrbp/api/now/table/sn_hr_core_employee_certification_and_license":{"get":{"summary":"ServiceNow Table API parity: an employee's certification/license records (sysparm_query=employee=<sys_id>^active=true). Supports the dot-walked certification_or_license.governing_agency field via sysparm_fields; wraps in {result: [...]}. Always shaped as sysparm_display_value=true + sysparm_exclude_reference_link=true (the emulated action sends both as true; not a toggle here — see README).","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"query","name":"sysparm_query","required":false,"schema":{"type":"string"},"example":"employee=12cf2eef933d0390f657fdab3603d6b3^active=true"},{"in":"query","name":"sysparm_fields","required":false,"schema":{"type":"string"},"example":"certification_or_license,certification_or_license.governing_agency,issue_date,expiration_date,status,employee,sys_id"},{"in":"query","name":"sysparm_limit","required":false,"schema":{"type":"integer"}},{"in":"query","name":"sysparm_offset","required":false,"schema":{"type":"integer"}}],"responses":{"200":{"description":"ServiceNow Table API parity: an employee's certification/license records (sysparm_query=employee=<sys_id>^active=true). Supports the dot-walked certification_or_license.governing_agency field via sysparm_fields; wraps in {result: [...]}. Always shaped as sysparm_display_value=true + sysparm_exclude_reference_link=true (the emulated action sends both as true; not a toggle here — see README)."},"400":{"description":"Bad request"}},"tags":["hrbp"]}},"/purple-suite/hrbp/api/now/table/sn_hr_core_profile":{"get":{"summary":"ServiceNow Table API parity: look up an employee's HR profile by name (sysparm_query=user.name=<value>); reference fields (user.department/user.manager/user.location) as {display_value, link}; wraps in {result: [...]}","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"}],"responses":{"200":{"description":"ServiceNow Table API parity: look up an employee's HR profile by name (sysparm_query=user.name=<value>); reference fields (user.department/user.manager/user.location) as {display_value, link}; wraps in {result: [...]}"},"400":{"description":"Bad request"}},"tags":["hrbp"]}},"/purple-suite/hrbp/api/now/table/sys_user_has_skill":{"get":{"summary":"ServiceNow Table API parity: list an employee's skills by sys_id (sysparm_query=user.sys_id=<value>); skill/skill_level as {display_value, link} (skill_level is \"\" when unset); wraps in {result: [...]}","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"}],"responses":{"200":{"description":"ServiceNow Table API parity: list an employee's skills by sys_id (sysparm_query=user.sys_id=<value>); skill/skill_level as {display_value, link} (skill_level is \"\" when unset); wraps in {result: [...]}"},"400":{"description":"Bad request"}},"tags":["hrbp"]}},"/purple-suite/hrbp/er_cases":{"get":{"summary":"Look up an ER case by number, or count ER cases by allegation type / department / location / leader / hr_service / date range (SN HRERCaseAPI parity)","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"}],"responses":{"200":{"description":"Look up an ER case by number, or count ER cases by allegation type / department / location / leader / hr_service / date range (SN HRERCaseAPI parity)"},"400":{"description":"Bad request"}},"tags":["hrbp"]}},"/purple-suite/hrbp/hrbp_batch_user_resolution/resolve":{"get":{"summary":"Batch resolve user names to emails for meeting scheduling (SN hrbp_batch_user_resolution parity)","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"}],"responses":{"200":{"description":"Batch resolve user names to emails for meeting scheduling (SN hrbp_batch_user_resolution parity)"},"400":{"description":"Bad request"}},"tags":["hrbp"]}},"/purple-suite/hrbp/hrbp_capabilities":{"get":{"summary":"Which optional HRBP Hub modules (e.g. Talent Pulse) are available on this instance (SN hrbp_capabilities parity)","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"}],"responses":{"200":{"description":"Which optional HRBP Hub modules (e.g. Talent Pulse) are available on this instance (SN hrbp_capabilities parity)"},"400":{"description":"Bad request"}},"tags":["hrbp"]}},"/purple-suite/hrbp/hrbp_manager_span":{"get":{"summary":"Manager span of control. With manager_sys_id: that manager's direct + total (transitive) report counts — {manager_sys_id, num_direct_reports, num_total_reports}, 404 if no such user, zeroes if they are not an active manager. Without it: the CALLING HRBP's in-scope managers ranked by total reports, paginated 10 per page — {page_number, count, managers, average_total_reports} (count is this page's size); never accepts a caller-supplied hrbpSysId. Wraps success AND error bodies in {result:...} (every branch of the SN op routes through response.setBody()).","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"query","name":"manager_sys_id","required":false,"schema":{"type":"string"},"example":"EMP-0003"},{"in":"query","name":"page_number","required":false,"schema":{"type":"integer"},"example":1}],"responses":{"200":{"description":"Manager span of control. With manager_sys_id: that manager's direct + total (transitive) report counts — {manager_sys_id, num_direct_reports, num_total_reports}, 404 if no such user, zeroes if they are not an active manager. Without it: the CALLING HRBP's in-scope managers ranked by total reports, paginated 10 per page — {page_number, count, managers, average_total_reports} (count is this page's size); never accepts a caller-supplied hrbpSysId. Wraps success AND error bodies in {result:...} (every branch of the SN op routes through response.setBody())."},"400":{"description":"Bad request"}},"tags":["hrbp"]}},"/purple-suite/hrbp/talent_reviews":{"get":{"summary":"List talent reviews","description":"A talent review is the HRBP's saved analytics package: a named review over a scope (leaders / departments / locations) and period, carrying an ordered list of attached metrics (headcount, attrition_rate, 9-box distribution, ...). It is the record the tp_* ServiceNow-parity tools create and execute. Use this raw list to browse or audit review records; use tp_find_reviews / tp_search_reviews when resolving by leader or department NAME. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, ... because $ is not a legal tool-input key).","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/hrbp.TalentReview"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["hrbp"]},"post":{"summary":"Create a talent review record directly","description":"Writes a talent review row with its scope, period and metric list, bypassing the guided flow. Prefer tp_preview_review + tp_commit_review (or tp_create_review_from_template), which validate the HRBP's scope access and de-duplicate via an idempotency key.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.TalentReview"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.TalentReview"}}}}},"tags":["hrbp"]}},"/purple-suite/hrbp/talent_reviews/{id}":{"get":{"summary":"Get a talent review by id","description":"Returns one review's definition - scope, period and attached metrics - but NOT computed results. Use tp_display_review or tp_execute_review to get metric values.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.TalentReview"}}}},"404":{"description":"Not found"}},"tags":["hrbp"]},"patch":{"summary":"Edit a talent review record","description":"Partial update of a review's name, scope, period or attached metric list. To add a metric to an existing review prefer tp_add_metric, which is idempotent on (review, metric).","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.TalentReview"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.TalentReview"}}}},"404":{"description":"Not found"}},"tags":["hrbp"]},"delete":{"summary":"Delete a talent review","description":"DESTRUCTIVE and irreversible: removes the review and its attached metric configuration. Any snapshots already captured from it survive as separate records. Confirm with the user before calling.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["hrbp"]}},"/purple-suite/hrbp/employee_assessments":{"get":{"summary":"List 9-box employee assessments","description":"An employee assessment is one employee's talent-review datapoint for a cycle: performance_rating (1-5) and derived performance_band, potential, flight_risk, replacement_readiness, achievable_level and the derived 9-box cell. This is HRBP calibration data, distinct from HRIS performance reviews (the formal review record) and from performance_reviews here (manager/employee rating pairs). Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, ... because $ is not a legal tool-input key).","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/hrbp.EmployeeAssessment"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["hrbp"]},"post":{"summary":"Create an employee assessment (9-box datapoint)","description":"Records one employee's calibration for a cycle, optionally tied to a talent_review_id. `box` is derived from potential x performance - do not set it.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.EmployeeAssessment"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.EmployeeAssessment"}}}}},"tags":["hrbp"]}},"/purple-suite/hrbp/employee_assessments/{id}":{"get":{"summary":"Get an employee assessment by id","description":"Returns one employee's assessment for a cycle, including the derived 9-box cell, core competencies and development needs.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.EmployeeAssessment"}}}},"404":{"description":"Not found"}},"tags":["hrbp"]},"patch":{"summary":"Edit an employee assessment","description":"Partial update of a calibration datapoint (rating, potential, flight risk, readiness). The 9-box cell is recomputed by the server.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.EmployeeAssessment"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.EmployeeAssessment"}}}},"404":{"description":"Not found"}},"tags":["hrbp"]},"delete":{"summary":"Delete an employee assessment","description":"DESTRUCTIVE and irreversible: removes the employee's calibration datapoint for that cycle, changing any 9-box or performance-distribution metric computed afterwards.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["hrbp"]}},"/purple-suite/hrbp/performance_reviews":{"get":{"summary":"List performance review ratings","description":"A performance review here is a lightweight rating pair for one employee over a review window: rating_manager and rating_employee (1-5) plus the assessment date. It feeds HRBP performance metrics. Narrower than an HRIS employee review (strengths/gaps narrative) and than an employee assessment (9-box potential and flight risk). Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, ... because $ is not a legal tool-input key).","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/hrbp.PerformanceReview"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["hrbp"]},"post":{"summary":"Create a performance review rating","description":"Records a manager and/or self rating for an employee over a review period. Use employee_assessments instead when you need potential, flight risk or 9-box placement.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.PerformanceReview"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.PerformanceReview"}}}}},"tags":["hrbp"]}},"/purple-suite/hrbp/performance_reviews/{id}":{"get":{"summary":"Get a performance review rating by id","description":"Returns one rating record: the employee, the review window and the manager/employee scores.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.PerformanceReview"}}}},"404":{"description":"Not found"}},"tags":["hrbp"]},"patch":{"summary":"Edit a performance review rating","description":"Partial update of the review window or the manager/employee ratings.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.PerformanceReview"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.PerformanceReview"}}}},"404":{"description":"Not found"}},"tags":["hrbp"]},"delete":{"summary":"Delete a performance review rating","description":"DESTRUCTIVE and irreversible: removes the rating record, which changes any performance metric recomputed afterwards.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["hrbp"]}},"/purple-suite/hrbp/succession_plans":{"get":{"summary":"List succession plans for critical roles","description":"A succession plan covers one critical ROLE (title, incumbent, department) with its criticality, loss_impact, retention_risk, success profile and a count of named successors. The candidates themselves are separate successor records linked by succession_plan_id. Use this to answer 'which roles have succession coverage'. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, ... because $ is not a legal tool-input key).","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/hrbp.SuccessionPlan"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["hrbp"]},"post":{"summary":"Create a succession plan for a role","description":"Opens a plan for a critical role and its incumbent. Add candidates afterwards as successor records; successor_count is maintained by the server.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.SuccessionPlan"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.SuccessionPlan"}}}}},"tags":["hrbp"]}},"/purple-suite/hrbp/succession_plans/{id}":{"get":{"summary":"Get a succession plan by id","description":"Returns one role's plan - incumbent, criticality, loss impact, retention risk and success profile. Candidate details live in the successors collection filtered by succession_plan_id.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.SuccessionPlan"}}}},"404":{"description":"Not found"}},"tags":["hrbp"]},"patch":{"summary":"Edit a succession plan","description":"Partial update of the plan's role metadata, risk ratings or success profile. Adding or ranking candidates is done on successor records, not here.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.SuccessionPlan"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.SuccessionPlan"}}}},"404":{"description":"Not found"}},"tags":["hrbp"]},"delete":{"summary":"Delete a succession plan","description":"DESTRUCTIVE and irreversible: removes the plan. Its successor records are orphaned rather than deleted. Prefer setting `active` to false to retire a plan.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["hrbp"]}},"/purple-suite/hrbp/successors":{"get":{"summary":"List succession candidates","description":"A successor is one named candidate on a succession plan: candidate_employee_id, top_candidate flag, readiness (ready now / 3 / 6 / 12 months), potential, current rating, matching score and development areas. Filter by succession_plan_id to see a role's bench; filter by candidate_employee_id to see which roles a person is slated for. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, ... because $ is not a legal tool-input key).","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/hrbp.Successor"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["hrbp"]},"post":{"summary":"Add a candidate to a succession plan","description":"Names an employee as a successor for the plan given by succession_plan_id, with their readiness and potential. The plan's successor_count is updated by the server.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.Successor"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.Successor"}}}}},"tags":["hrbp"]}},"/purple-suite/hrbp/successors/{id}":{"get":{"summary":"Get a succession candidate by id","description":"Returns one candidate's bench entry: readiness, potential, achievable level, match score and development areas.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.Successor"}}}},"404":{"description":"Not found"}},"tags":["hrbp"]},"patch":{"summary":"Edit a succession candidate","description":"Partial update of a candidate's readiness, potential, top_candidate flag or development areas - the usual way to re-rank a bench after calibration.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.Successor"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.Successor"}}}},"404":{"description":"Not found"}},"tags":["hrbp"]},"delete":{"summary":"Remove a candidate from a succession plan","description":"DESTRUCTIVE and irreversible: deletes the bench entry and lowers the plan's coverage. The employee record itself is untouched.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["hrbp"]}},"/purple-suite/hrbp/snapshots":{"get":{"summary":"List captured metric snapshots","description":"A snapshot is one metric value frozen at a point in time: metric_id, the computed value JSON (per group), a source and a timestamp, batched under a group_id. Snapshots are the history that makes trend questions answerable; to compute a metric live use tp_execute_metric or tp_query_metric instead. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, ... because $ is not a legal tool-input key).","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/hrbp.Snapshot"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["hrbp"]},"post":{"summary":"Capture a metric snapshot","description":"Stores a computed metric value against a metric_id and group_id. Normally written by the snapshot pipeline - creating one by hand inserts a datapoint into the trend history.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.Snapshot"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.Snapshot"}}}}},"tags":["hrbp"]}},"/purple-suite/hrbp/snapshots/{id}":{"get":{"summary":"Get a metric snapshot by id","description":"Returns one frozen metric value with its timestamp, source and per-group breakdown.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.Snapshot"}}}},"404":{"description":"Not found"}},"tags":["hrbp"]},"patch":{"summary":"Edit a metric snapshot","description":"Partial update of a stored datapoint. Editing history rewrites what trend queries will report - prefer capturing a new snapshot.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.Snapshot"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.Snapshot"}}}},"404":{"description":"Not found"}},"tags":["hrbp"]},"delete":{"summary":"Delete a metric snapshot","description":"DESTRUCTIVE and irreversible: removes a datapoint from the trend history, which changes previously reported trends.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["hrbp"]}},"/purple-suite/hrbp/metric_goals":{"get":{"summary":"List metric goals (targets and thresholds)","description":"A metric goal is a target set on a metric for a period: metric_id, period_start/end, threshold_value and a comparison_type saying whether being above or below the threshold is good. Optionally scoped to a talent review. Read these to judge whether a computed metric is on or off target. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, ... because $ is not a legal tool-input key).","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/hrbp.MetricGoal"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["hrbp"]},"post":{"summary":"Create a metric goal","description":"Sets a target threshold for a metric over a period (e.g. attrition_rate below 3% for FY26), optionally tied to a talent review. Defines the bar; it does not compute anything.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.MetricGoal"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.MetricGoal"}}}}},"tags":["hrbp"]}},"/purple-suite/hrbp/metric_goals/{id}":{"get":{"summary":"Get a metric goal by id","description":"Returns one target: its metric, period, threshold value and comparison direction.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.MetricGoal"}}}},"404":{"description":"Not found"}},"tags":["hrbp"]},"patch":{"summary":"Edit a metric goal","description":"Partial update of a target's threshold, period or comparison direction - i.e. moving the bar.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.MetricGoal"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.MetricGoal"}}}},"404":{"description":"Not found"}},"tags":["hrbp"]},"delete":{"summary":"Delete a metric goal","description":"DESTRUCTIVE and irreversible: removes the target, after which the metric has no threshold to be judged against. Prefer setting `active` to false.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["hrbp"]}},"/purple-suite/hrbp/hrbp_scope_assignments":{"get":{"summary":"List HRBP scope assignments (who can see what)","description":"A scope assignment grants ONE HRBP user access to one scope - by scope_type/scope_id, or all_access for an unrestricted HR leader. This is the access-control table that tp_resolve_scope checks before any metric or review runs, and it is why a query can come back access_denied. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, ... because $ is not a legal tool-input key).","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/hrbp.HrbpScopeAssignment"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["hrbp"]},"post":{"summary":"Grant an HRBP access to a scope","description":"Assigns a user a scope (or all_access). SECURITY-SENSITIVE: this widens who can see employee talent data. Confirm with the user before calling.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.HrbpScopeAssignment"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.HrbpScopeAssignment"}}}}},"tags":["hrbp"]}},"/purple-suite/hrbp/hrbp_scope_assignments/{id}":{"get":{"summary":"Get an HRBP scope assignment by id","description":"Returns one grant: the user, the scope type/id it covers, whether it is all_access, and whether it is active.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.HrbpScopeAssignment"}}}},"404":{"description":"Not found"}},"tags":["hrbp"]},"patch":{"summary":"Edit an HRBP scope assignment","description":"Partial update of a grant (repoint the scope, toggle all_access or active). SECURITY-SENSITIVE - it changes what employee data that HRBP can see.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.HrbpScopeAssignment"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.HrbpScopeAssignment"}}}},"404":{"description":"Not found"}},"tags":["hrbp"]},"delete":{"summary":"Revoke an HRBP scope assignment","description":"DESTRUCTIVE and irreversible: removes the grant, so that HRBP loses access to the scope and their queries may start returning access_denied.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["hrbp"]}},"/purple-suite/hrbp/hrbp_scopes":{"get":{"summary":"List HRBP scopes (named population definitions)","description":"An HRBP scope is a NAMED population of employees (e.g. 'R&D - AMS') whose membership is defined by its hrbp_scope_rules. Scopes are what scope assignments hand out to HRBP users. Use this to see which populations exist before assigning one. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, ... because $ is not a legal tool-input key).","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/hrbp.HrbpScope"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["hrbp"]},"post":{"summary":"Create an HRBP scope","description":"Defines a new named population. It matches nobody until you add hrbp_scope_rules to it.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.HrbpScope"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.HrbpScope"}}}}},"tags":["hrbp"]}},"/purple-suite/hrbp/hrbp_scopes/{id}":{"get":{"summary":"Get an HRBP scope by id","description":"Returns one scope's name, description and active flag. Its membership criteria are the hrbp_scope_rules with this scope_id.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.HrbpScope"}}}},"404":{"description":"Not found"}},"tags":["hrbp"]},"patch":{"summary":"Edit an HRBP scope","description":"Partial update of a scope's name, description or active flag. Membership is changed by editing its rules, not here.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.HrbpScope"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.HrbpScope"}}}},"404":{"description":"Not found"}},"tags":["hrbp"]},"delete":{"summary":"Delete an HRBP scope","description":"DESTRUCTIVE and irreversible: removes the population definition. Its rules and any assignments pointing at it are left dangling, so HRBPs may lose access. Confirm with the user before calling.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["hrbp"]}},"/purple-suite/hrbp/hrbp_scope_rules":{"get":{"summary":"List HRBP scope membership rules","description":"A scope rule is one membership clause of an HRBP scope: a dimension (department, location, leader, ...) and the value that must match. Rules combine to define who the parent scope covers. Filter by scope_id to read one population's definition. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, ... because $ is not a legal tool-input key).","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/hrbp.HrbpScopeRule"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["hrbp"]},"post":{"summary":"Add a membership rule to an HRBP scope","description":"Adds a dimension/value clause to the scope given by scope_id, widening or narrowing who that population covers. SECURITY-SENSITIVE: it changes what data the scope's HRBPs can reach.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.HrbpScopeRule"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.HrbpScopeRule"}}}}},"tags":["hrbp"]}},"/purple-suite/hrbp/hrbp_scope_rules/{id}":{"get":{"summary":"Get an HRBP scope rule by id","description":"Returns one membership clause: its parent scope, the dimension it matches on, and the value.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.HrbpScopeRule"}}}},"404":{"description":"Not found"}},"tags":["hrbp"]},"patch":{"summary":"Edit an HRBP scope rule","description":"Partial update of a clause's dimension, value or active flag. SECURITY-SENSITIVE - it silently changes the population an HRBP can see.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.HrbpScopeRule"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.HrbpScopeRule"}}}},"404":{"description":"Not found"}},"tags":["hrbp"]},"delete":{"summary":"Delete an HRBP scope rule","description":"DESTRUCTIVE and irreversible: removes a membership clause, shrinking (or, if it was the last exclusion, widening) the parent scope.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["hrbp"]}},"/purple-suite/hrbp/snapshot_groups":{"get":{"summary":"List snapshot capture batches","description":"A snapshot group is one capture run - a named, timestamped batch (e.g. 'March 2026 workforce snapshot') that the individual snapshot records hang off via group_id. Use this to find which capture runs exist, then read snapshots for the values. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, ... because $ is not a legal tool-input key).","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/hrbp.SnapshotGroup"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["hrbp"]},"post":{"summary":"Create a snapshot capture batch","description":"Opens a named, timestamped batch that snapshot records can be filed under. It captures no metrics by itself.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.SnapshotGroup"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.SnapshotGroup"}}}}},"tags":["hrbp"]}},"/purple-suite/hrbp/snapshot_groups/{id}":{"get":{"summary":"Get a snapshot capture batch by id","description":"Returns one batch's name, capture timestamp, source and note. The metric values are the snapshots carrying this group_id.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.SnapshotGroup"}}}},"404":{"description":"Not found"}},"tags":["hrbp"]},"patch":{"summary":"Edit a snapshot capture batch","description":"Partial update of a batch's name, note or source. Does not change the snapshot values inside it.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.SnapshotGroup"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.SnapshotGroup"}}}},"404":{"description":"Not found"}},"tags":["hrbp"]},"delete":{"summary":"Delete a snapshot capture batch","description":"DESTRUCTIVE and irreversible: removes the batch header. Its snapshots survive but lose their grouping.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["hrbp"]}},"/purple-suite/hrbp/metric_sources":{"get":{"summary":"List metric data sources","description":"A metric source is a registered system that talent metrics can be computed from (e.g. 'local' = Purple HRIS). Small reference table: it records where numbers come from, not the numbers themselves. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, ... because $ is not a legal tool-input key).","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/hrbp.MetricSource"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["hrbp"]},"post":{"summary":"Register a metric data source","description":"Adds a source system that snapshots and metric computations can attribute to. Administrative configuration.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.MetricSource"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.MetricSource"}}}}},"tags":["hrbp"]}},"/purple-suite/hrbp/metric_sources/{id}":{"get":{"summary":"Get a metric data source by id","description":"Returns one registered source: its key, display name and active flag.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.MetricSource"}}}},"404":{"description":"Not found"}},"tags":["hrbp"]},"patch":{"summary":"Edit a metric data source","description":"Partial update of a source's name, description or active flag.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.MetricSource"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.MetricSource"}}}},"404":{"description":"Not found"}},"tags":["hrbp"]},"delete":{"summary":"Delete a metric data source","description":"DESTRUCTIVE and irreversible: removes the source registration. Snapshots that referenced it keep the key but it no longer resolves.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["hrbp"]}},"/purple-suite/hrbp/employee_relations":{"get":{"summary":"List employee relations (ER) cases","description":"An ER case is an HR investigation or performance-management record about one subject_person: short description, state, priority, the HR rep who opened it and the investigator assigned, due/closed dates, plus PIP fields (start/end, improvement areas, outcome). SENSITIVE HR data, and separate from the talent-review side of this app. Allegations and involved parties are child records keyed by employee_relation_id. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, ... because $ is not a legal tool-input key).","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/hrbp.EmployeeRelation"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["hrbp"]},"post":{"summary":"Open an employee relations (ER) case","description":"Creates an investigation / performance-management case about an employee. SENSITIVE: it starts a formal HR record about a named person - confirm with the user before calling.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.EmployeeRelation"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.EmployeeRelation"}}}}},"tags":["hrbp"]}},"/purple-suite/hrbp/employee_relations/{id}":{"get":{"summary":"Get an employee relations (ER) case by id","description":"Returns one case with its state, priority, assignment, dates and PIP details. SENSITIVE HR content; a locked case restricts who may read it.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.EmployeeRelation"}}}},"404":{"description":"Not found"}},"tags":["hrbp"]},"patch":{"summary":"Edit an employee relations (ER) case","description":"Partial update of a case - reassign the investigator, change state or priority, close it, or record PIP progress and outcome. SENSITIVE HR record.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.EmployeeRelation"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.EmployeeRelation"}}}},"404":{"description":"Not found"}},"tags":["hrbp"]},"delete":{"summary":"Delete an employee relations (ER) case","description":"DESTRUCTIVE and irreversible: erases the investigation record and its audit trail, and orphans its allegations and involved parties. Prefer closing the case (state) over deleting it.","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["hrbp"]}},"/purple-suite/hrbp/allegation_types":{"get":{"summary":"List allegation types","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/hrbp.AllegationType"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["hrbp"]},"post":{"summary":"Create allegation type","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.AllegationType"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.AllegationType"}}}}},"tags":["hrbp"]}},"/purple-suite/hrbp/allegation_types/{id}":{"get":{"summary":"Get allegation type","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.AllegationType"}}}},"404":{"description":"Not found"}},"tags":["hrbp"]},"patch":{"summary":"Update allegation type","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.AllegationType"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.AllegationType"}}}},"404":{"description":"Not found"}},"tags":["hrbp"]},"delete":{"summary":"Delete allegation type","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["hrbp"]}},"/purple-suite/hrbp/allegation_subtypes":{"get":{"summary":"List allegation subtypes","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/hrbp.AllegationSubtype"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["hrbp"]},"post":{"summary":"Create allegation subtype","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.AllegationSubtype"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.AllegationSubtype"}}}}},"tags":["hrbp"]}},"/purple-suite/hrbp/allegation_subtypes/{id}":{"get":{"summary":"Get allegation subtype","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.AllegationSubtype"}}}},"404":{"description":"Not found"}},"tags":["hrbp"]},"patch":{"summary":"Update allegation subtype","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.AllegationSubtype"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.AllegationSubtype"}}}},"404":{"description":"Not found"}},"tags":["hrbp"]},"delete":{"summary":"Delete allegation subtype","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["hrbp"]}},"/purple-suite/hrbp/allegations":{"get":{"summary":"List allegations","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/hrbp.Allegation"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["hrbp"]},"post":{"summary":"Create allegation","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.Allegation"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.Allegation"}}}}},"tags":["hrbp"]}},"/purple-suite/hrbp/allegations/{id}":{"get":{"summary":"Get allegation","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.Allegation"}}}},"404":{"description":"Not found"}},"tags":["hrbp"]},"patch":{"summary":"Update allegation","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.Allegation"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.Allegation"}}}},"404":{"description":"Not found"}},"tags":["hrbp"]},"delete":{"summary":"Delete allegation","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["hrbp"]}},"/purple-suite/hrbp/involved_parties":{"get":{"summary":"List involved parties","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/hrbp.InvolvedParty"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["hrbp"]},"post":{"summary":"Create involved party","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.InvolvedParty"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.InvolvedParty"}}}}},"tags":["hrbp"]}},"/purple-suite/hrbp/involved_parties/{id}":{"get":{"summary":"Get involved party","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.InvolvedParty"}}}},"404":{"description":"Not found"}},"tags":["hrbp"]},"patch":{"summary":"Update involved party","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.InvolvedParty"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hrbp.InvolvedParty"}}}},"404":{"description":"Not found"}},"tags":["hrbp"]},"delete":{"summary":"Delete involved party","parameters":[{"$ref":"#/components/parameters/hrbp.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["hrbp"]}},"/purple-suite/hris/employees/search":{"post":{"summary":"Search HRIS employees by name, email or attributes","description":"Fuzzy person lookup over the HRIS employee master (the record of employment: manager, department, title, salary band, status) - not the identity directory or a chat user roster. Pass `query` for scored free-text search OR framed fields (name, email, department, role, manager_name, employment_status) for structured search; the two are mutually exclusive. Returns RecordResult envelopes with a nested person_entity. Reach for this when you have a partial or misspelled name; use the employees list when you can express the criteria as an exact $filter.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"query":{"type":"string","description":"Free-text search query (tokenized, scored by term overlap). Mutually exclusive with framed fields.","example":"Alice Engineering"},"name":{"type":"string","description":"Full or partial person name — firstName + lastName substring match (framed search)","example":"Alice Smith"},"email":{"type":"string","description":"Email address partial match (framed search)","example":"alice.smith"},"department":{"type":"string","description":"Exact department name match (framed search)","example":"Engineering"},"role":{"type":"string","description":"Role/title substring match (framed search)","example":"Engineer"},"manager_name":{"type":"string","description":"Manager full name substring — applied as post-filter after framed scoring (framed search)","example":"Bob Johnson"},"manager_email":{"type":"string","description":"Manager email substring — applied as post-filter after framed scoring (framed search)","example":"bob.johnson"},"employment_status":{"type":"string","enum":["active","terminated","on_leave"],"description":"Employment status exact match (framed search)","example":"active"}}}}}},"responses":{"200":{"description":"Search results in RecordResult format (top 5 matches by score)","content":{"application/json":{"schema":{"type":"object","properties":{"results":{"type":"array","items":{"type":"object","properties":{"record":{"type":"object","description":"Nested RecordResult envelope","properties":{"meta_info":{"type":"object","properties":{"structured_entity":{"type":"object","properties":{"person_entity":{"type":"object","properties":{"person_info":{"type":"object","properties":{"full_name":{"type":"string"},"employee_id":{"type":"string"}}},"contact_info":{"type":"object","properties":{"email":{"type":"string"},"phone_number":{"type":"string","nullable":true},"aliases":{"type":"array","items":{"type":"string"}}}},"occupation_info":{"type":"object","properties":{"role":{"type":"string"},"manager":{"type":"object","nullable":true,"properties":{"full_name":{"type":"string"},"email":{"type":"string"}}},"direct_reports":{"type":"array","items":{"type":"object"}}}},"workplace_info":{"type":"object","properties":{"department":{"type":"string"},"timezone":{"type":"string","nullable":true},"location":{"type":"string","nullable":true},"cost_center":{"type":"string","nullable":true}}}}}}}}}}},"score":{"type":"number","description":"Match score between 0 and 1"},"confidence":{"type":"string","enum":["EXACT_MATCH","VALID","INVALID"],"description":"EXACT_MATCH ≥ 0.9, VALID ≥ 0.5, INVALID < 0.5"}}}},"total_retrieved":{"type":"integer"}}}}}},"500":{"description":"Internal server error"}},"tags":["hris"]}},"/purple-suite/hris/users/search":{"post":{"summary":"Search HRIS employees, projected as flat User records","description":"Identical search semantics to the employee search (free-text `query` OR framed fields) but each match comes back as a flat User-shaped object (email_addr, full_name, record_id, ...) instead of the nested RecordResult envelope. Choose this only when the caller expects native-User field names; otherwise prefer the employee search. It still reads the HRIS employee master, not an identity or chat directory.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"query":{"type":"string","description":"Free-text search query (tokenized, scored by term overlap). Mutually exclusive with framed fields.","example":"Alice Engineering"},"name":{"type":"string","description":"Full or partial person name — firstName + lastName substring match (framed search)","example":"Alice Smith"},"email":{"type":"string","description":"Email address partial match (framed search)","example":"alice.smith"},"department":{"type":"string","description":"Exact department name match (framed search)","example":"Engineering"},"role":{"type":"string","description":"Role/title substring match (framed search)","example":"Engineer"},"manager_name":{"type":"string","description":"Manager full name substring — applied as post-filter after framed scoring (framed search)","example":"Bob Johnson"},"manager_email":{"type":"string","description":"Manager email substring — applied as post-filter after framed scoring (framed search)","example":"bob.johnson"},"employment_status":{"type":"string","enum":["active","terminated","on_leave"],"description":"Employment status exact match (framed search)","example":"active"}}}}}},"responses":{"200":{"description":"Search results as flat User records (top 5 matches by score)","content":{"application/json":{"schema":{"type":"object","properties":{"results":{"type":"array","items":{"type":"object","properties":{"record_id":{"type":"string","description":"Employee id","example":"EMP-001"},"email_addr":{"type":"string","example":"alice.smith@example.com"},"full_name":{"type":"string","example":"Alice Smith"},"first_name":{"type":"string","example":"Alice"},"last_name":{"type":"string","example":"Smith"},"department":{"type":"string","nullable":true,"example":"Engineering"},"role":{"type":"string","nullable":true,"example":"Software Engineer"},"manager_email":{"type":"string","nullable":true,"example":"bob.johnson@example.com"},"timezone":{"type":"string","nullable":true,"example":"America/Los_Angeles"},"location":{"type":"string","nullable":true,"example":"Mountain View"}}}},"total_retrieved":{"type":"integer"}}}}}},"500":{"description":"Internal server error"}},"tags":["hris"]}},"/purple-suite/hris/employees":{"get":{"summary":"List HRIS employee records","description":"An HRIS employee is the system-of-record employment file for a person: employee id, name/email, manager, department, title, hire date, salary and compensation, employment status (active / on_leave / terminated / suspended), PTO balance, skills, reviews and goals. This is the HR master data - richer and more sensitive than the identity directory (a people/account lookup) or a chat user roster. Use it for org, headcount, compensation and status questions; use the employee search for fuzzy name matching. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, ... because $ is not a legal tool-input key).","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Max items per page (default 50)"},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Pass the previous response's nextCursor to fetch the next page"},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."},{"in":"query","name":"status","required":false,"schema":{"type":"string","enum":["active","terminated","on_leave"]},"description":"Filter by employment status (exact match). Legacy — prefer $filter.","example":"active"},{"in":"query","name":"department","required":false,"schema":{"type":"string"},"description":"Filter by department name (exact match). Legacy — prefer $filter.","example":"Engineering"},{"in":"query","name":"firstName","required":false,"schema":{"type":"string"},"description":"Filter by first name (exact match). Legacy — prefer $filter.","example":"Alice"},{"in":"query","name":"lastName","required":false,"schema":{"type":"string"},"description":"Filter by last name (exact match). Legacy — prefer $filter.","example":"Smith"},{"in":"query","name":"email","required":false,"schema":{"type":"string"},"description":"Filter by email address (exact match). Legacy — prefer $filter.","example":"alice.smith0@acme.com"},{"in":"query","name":"managerId","required":false,"schema":{"type":"string"},"description":"Filter by manager employee ID (exact match). Legacy — prefer $filter.","example":"EMP-0003"},{"in":"query","name":"search","required":false,"schema":{"type":"string"},"description":"Free-text search across firstName, lastName, and email (substring match). Legacy — equivalent to $search.","example":"alice"}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/hris.Employee"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page"},"total":{"type":"integer","description":"Total documents in the collection"}}}}}}},"tags":["hris"]},"post":{"summary":"Create an HRIS employee record","description":"Onboards a person into the HR system of record with their employment details. Email must be unique in the instance (409 otherwise). This creates the employment file only - it does not provision identity, chat or any downstream account.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.Employee"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.Employee"}}}},"400":{"description":"Missing required field or invalid status"},"409":{"description":"Employee with this email already exists"}},"tags":["hris"]}},"/purple-suite/hris/employees/{id}":{"get":{"summary":"Get an HRIS employee record by id","description":"Returns one employment file in full - manager, department, title, compensation, status history, PTO balance, skills, reviews and goals. Use the employee search when you have a name rather than an id.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.Employee"}}}},"404":{"description":"Not found"}},"tags":["hris"]},"patch":{"summary":"Update an HRIS employee record","description":"Partial update of the employment file - also the way to write the `skills`, `reviews` and `goals` arrays that the dedicated GET sub-resources read back. Status accepts 'suspended' on top of the standard statuses, but terminated employees cannot be reactivated and their employment fields (salary, department_id, title) are frozen; for lifecycle moves prefer the transition action, and for PTO use the pto-balance action.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.Employee"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.Employee"}}}},"404":{"description":"Not found"},"422":{"description":"State machine violation (e.g. reactivating a terminated employee)"}},"tags":["hris"]},"delete":{"summary":"Delete an HRIS employee record","description":"DESTRUCTIVE and irreversible: erases the entire employment file including status history, reviews and PTO. This is NOT how you offboard someone - use the transition action to set status to `terminated`, which preserves the record. Confirm with the user before calling.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"id":{"type":"string"}}}}}},"404":{"description":"Not found"}},"tags":["hris"]}},"/purple-suite/hris/employees/{id}/transition":{"post":{"summary":"Move an employee through their lifecycle (leave / return / terminate)","description":"The correct way to change employment status: applies the state machine and appends to status_history with a reason and effective date. Terminated employees cannot be reactivated or put on leave, and an on_leave employee must return to active before termination. Optionally deducts `days` of PTO when going on_leave. Terminating is effectively irreversible - confirm with the user first. Use this rather than PATCHing `status` on the employee.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["active","terminated","on_leave"],"description":"Target status for the transition","example":"on_leave"},"reason":{"type":"string","nullable":true,"description":"Human-readable reason for the transition (appended to status_history)","example":"Medical leave"},"effective_date":{"type":"string","format":"date-time","nullable":true,"description":"When the transition takes effect (defaults to now)","example":"2026-06-01T00:00:00.000Z"},"days":{"type":"number","description":"PTO vacation days to deduct from pto_balance.vacation_days. Only applied when transitioning to on_leave.","example":5}},"required":["status"]}}}},"responses":{"200":{"description":"Updated employee record","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.Employee"}}}},"400":{"description":"Invalid status value"},"404":{"description":"Employee not found"},"422":{"description":"Transition not allowed by state machine rules"}},"tags":["hris"]}},"/purple-suite/hris/employees/{id}/pto-balance":{"patch":{"summary":"Set an employee's PTO day balances","description":"Directly SETS (does not add to) vacation_days and/or sick_days on the employee's pto_balance; omitted fields keep their current value. Use this for balance corrections and accrual grants - booking actual time off goes through time-off requests, and going on leave can deduct days via the transition action.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"vacation_days":{"type":"number","description":"New vacation day balance. Omit to keep current value.","example":15},"sick_days":{"type":"number","description":"New sick day balance. Omit to keep current value.","example":5}}}}}},"responses":{"200":{"description":"Updated employee record with new pto_balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.Employee"}}}},"404":{"description":"Employee not found"}},"tags":["hris"]}},"/purple-suite/hris/employees/{id}/skills":{"get":{"summary":"Get an employee's skills profile","description":"Read-only convenience view of the `skills` array on one employee record (skill, proficiency, last_updated) - useful for staffing and development questions. To change skills, PATCH the `skills` field on the employee; there is no write endpoint here.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Employee skills","content":{"application/json":{"schema":{"type":"object","properties":{"employee_id":{"type":"string"},"skills":{"type":"array","items":{"$ref":"#/components/schemas/hris.Employee/properties/skills/items"}}}}}}},"404":{"description":"Employee not found"}},"tags":["hris"]}},"/purple-suite/hris/employees/{id}/reviews":{"get":{"summary":"Get an employee's performance review history","description":"Read-only convenience view of the `reviews` array on one employee record (period, rating, strengths, gaps). This is the narrative review history on the employment file - distinct from review-cycles, which track the review PROCESS. To change reviews, PATCH the `reviews` field on the employee.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Employee reviews","content":{"application/json":{"schema":{"type":"object","properties":{"employee_id":{"type":"string"},"reviews":{"type":"array","items":{"$ref":"#/components/schemas/hris.Employee/properties/reviews/items"}}}}}}},"404":{"description":"Employee not found"}},"tags":["hris"]}},"/purple-suite/hris/employees/{id}/goals":{"get":{"summary":"Get an employee's development goals","description":"Read-only convenience view of the `goals` array on one employee record (description, target_role, target_timeline, status, progress_pct) - career-development plans, not company OKRs. To change goals, PATCH the `goals` field on the employee.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Employee goals","content":{"application/json":{"schema":{"type":"object","properties":{"employee_id":{"type":"string"},"goals":{"type":"array","items":{"$ref":"#/components/schemas/hris.Employee/properties/goals/items"}}}}}}},"404":{"description":"Employee not found"}},"tags":["hris"]}},"/purple-suite/hris/departments":{"get":{"summary":"List HRIS departments","description":"A department is an org unit in the HR system of record - name, its manager and its headcount. Employees reference it by department_id / department name. Use it to enumerate the org structure or resolve a department before filtering employees. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, ... because $ is not a legal tool-input key).","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Max items per page (default 50)"},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Pass the previous response's nextCursor to fetch the next page"},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."},{"in":"query","name":"name","required":false,"schema":{"type":"string"},"description":"Filter by department name (exact match)","example":"Engineering"}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/hris.Department"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page"},"total":{"type":"integer","description":"Total documents in the collection"}}}}}}},"tags":["hris"]},"post":{"summary":"Create an HRIS department","description":"Adds an org unit that employees can be assigned to. Creating a department does not move any employee into it - PATCH the employees for that.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.Department"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.Department"}}}}},"tags":["hris"]}},"/purple-suite/hris/departments/{id}":{"get":{"summary":"Get an HRIS department by id","description":"Returns one org unit with its manager and headcount. The employees in it are found by filtering the employees list on that department.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.Department"}}}},"404":{"description":"Not found"}},"tags":["hris"]},"patch":{"summary":"Edit an HRIS department","description":"Partial update of an org unit (rename, change its manager). Renaming does not automatically re-point employees that store the department by name.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.Department"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.Department"}}}},"404":{"description":"Not found"}},"tags":["hris"]},"delete":{"summary":"Delete an HRIS department","description":"DESTRUCTIVE and irreversible: removes the org unit. Employees that referenced it keep a department that no longer resolves. Confirm with the user before calling.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"id":{"type":"string"}}}}}},"404":{"description":"Not found"}},"tags":["hris"]}},"/purple-suite/hris/pay-runs":{"get":{"summary":"List payroll runs","description":"A pay run is one payroll cycle for the company - its pay period, pay date, employee count, gross/net totals and a status of draft -> processing -> completed (or cancelled). Company-level payroll, not an individual payslip; per-person hours live on timesheets. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, ... because $ is not a legal tool-input key).","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Max items per page (default 50)"},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Pass the previous response's nextCursor to fetch the next page"},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."},{"in":"query","name":"status","required":false,"schema":{"type":"string","enum":["draft","processing","completed","cancelled"]},"description":"Filter by pay run status (exact match)","example":"draft"}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/hris.PayRun"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page"},"total":{"type":"integer","description":"Total documents in the collection"}}}}}}},"tags":["hris"]},"post":{"summary":"Open a payroll run","description":"Creates a payroll cycle, normally in `draft`, for a pay period. Advance it with the update operation - it must pass through `processing` before it can be completed.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.PayRun"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.PayRun"}}}}},"tags":["hris"]}},"/purple-suite/hris/pay-runs/{id}":{"get":{"summary":"Get a payroll run by id","description":"Returns one payroll cycle: period, pay date, employee count, gross and net totals and current status.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.PayRun"}}}},"404":{"description":"Not found"}},"tags":["hris"]},"patch":{"summary":"Advance or edit a payroll run","description":"Partial update, and the way a run moves through its lifecycle. State machine: a completed run is LOCKED and cannot be edited, a cancelled run cannot be reactivated, and draft cannot jump straight to completed (it must pass through processing). Completing a run is effectively irreversible - confirm with the user.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.PayRun"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.PayRun"}}}},"404":{"description":"Not found"},"422":{"description":"State machine violation (e.g. modifying a completed run, or skipping processing status)"}},"tags":["hris"]},"delete":{"summary":"Delete a payroll run","description":"DESTRUCTIVE and irreversible: erases the payroll cycle and its totals. Prefer setting status to `cancelled`, which keeps the audit trail.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"id":{"type":"string"}}}}}},"404":{"description":"Not found"}},"tags":["hris"]}},"/purple-suite/hris/timesheets":{"get":{"summary":"List employee timesheets","description":"A timesheet is one employee's hours for one week (week start, regular and overtime hours) with a status of submitted / approved / rejected. Per-person time capture that feeds payroll; the payroll cycle itself is a pay run, and paid absence is a time-off request. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, ... because $ is not a legal tool-input key).","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Max items per page (default 50)"},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Pass the previous response's nextCursor to fetch the next page"},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."},{"in":"query","name":"employee_id","required":false,"schema":{"type":"string"},"description":"Filter by employee ID (exact match)","example":"EMP-0001"},{"in":"query","name":"status","required":false,"schema":{"type":"string","enum":["submitted","approved","rejected"]},"description":"Filter by timesheet status (exact match)","example":"submitted"}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/hris.Timesheet"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page"},"total":{"type":"integer","description":"Total documents in the collection"}}}}}}},"tags":["hris"]},"post":{"summary":"Submit a weekly timesheet","description":"Records an employee's hours for one week. One timesheet per employee per week (409 on a duplicate), hours are range-checked, and terminated employees are rejected with 422.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.Timesheet"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.Timesheet"}}}},"400":{"description":"Missing required field or out-of-range hours"},"404":{"description":"Employee not found"},"409":{"description":"Timesheet already exists for this employee and week"},"422":{"description":"Cannot submit timesheet for a terminated employee"}},"tags":["hris"]}},"/purple-suite/hris/timesheets/{id}":{"get":{"summary":"Get a timesheet by id","description":"Returns one employee-week of hours with its regular/overtime split and approval status.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.Timesheet"}}}},"404":{"description":"Not found"}},"tags":["hris"]},"patch":{"summary":"Approve, reject or correct a timesheet","description":"Partial update - the way a manager sets `status` to approved or rejected, and the way logged hours are corrected before payroll picks them up.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.Timesheet"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.Timesheet"}}}},"404":{"description":"Not found"}},"tags":["hris"]},"delete":{"summary":"Delete a timesheet","description":"DESTRUCTIVE and irreversible: removes the week's recorded hours. Prefer rejecting the timesheet, which keeps the record.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"id":{"type":"string"}}}}}},"404":{"description":"Not found"}},"tags":["hris"]}},"/purple-suite/hris/benefits":{"get":{"summary":"List employee benefit enrollments","description":"A benefit record is one employee's ENROLLMENT in a plan - the benefit type (insurance / retirement / wellness / health), plan name, coverage level, employee and employer contributions and a status of active / pending / terminated. These are per-person enrollments, not a catalog of available plans. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, ... because $ is not a legal tool-input key).","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Max items per page (default 50)"},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Pass the previous response's nextCursor to fetch the next page"},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."},{"in":"query","name":"employee_id","required":false,"schema":{"type":"string"},"description":"Filter by employee ID (exact match)","example":"EMP-0001"},{"in":"query","name":"type","required":false,"schema":{"type":"string","enum":["insurance","retirement","wellness","health"]},"description":"Filter by benefit type (exact match)","example":"insurance"},{"in":"query","name":"status","required":false,"schema":{"type":"string","enum":["active","pending","terminated"]},"description":"Filter by benefit status (exact match)","example":"active"}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/hris.Benefit"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page"},"total":{"type":"integer","description":"Total documents in the collection"}}}}}}},"tags":["hris"]},"post":{"summary":"Enroll an employee in a benefit plan","description":"Creates an enrollment for one employee. An employee may hold only one ACTIVE benefit of a given type (422 otherwise), so terminate the existing enrollment before switching plans.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.Benefit"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.Benefit"}}}},"400":{"description":"Missing required field or invalid type"},"404":{"description":"Employee not found"},"422":{"description":"Employee already has an active benefit of this type"}},"tags":["hris"]}},"/purple-suite/hris/benefits/{id}":{"get":{"summary":"Get a benefit enrollment by id","description":"Returns one employee's enrollment: plan, coverage level, contributions, effective dates and status.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.Benefit"}}}},"404":{"description":"Not found"}},"tags":["hris"]},"patch":{"summary":"Change or terminate a benefit enrollment","description":"Partial update of one enrollment - change coverage or contributions, or set `status` to terminated to end it while keeping the history.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.Benefit"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.Benefit"}}}},"404":{"description":"Not found"}},"tags":["hris"]},"delete":{"summary":"Delete a benefit enrollment","description":"DESTRUCTIVE and irreversible: erases the enrollment and its history. Prefer setting `status` to terminated.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"id":{"type":"string"}}}}}},"404":{"description":"Not found"}},"tags":["hris"]}},"/purple-suite/hris/review-cycles":{"get":{"summary":"List performance review cycles","description":"A review cycle tracks the performance-review PROCESS for an employee in a period: reviewer, due date, overall rating and a status of planning / in_progress / completed / cancelled. Use it for 'is my review done / who is my reviewer'; the narrative outcomes live in the employee's reviews sub-resource. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, ... because $ is not a legal tool-input key).","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Max items per page (default 50)"},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Pass the previous response's nextCursor to fetch the next page"},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."},{"in":"query","name":"employee_id","required":false,"schema":{"type":"string"},"description":"Filter by employee ID (exact match)","example":"EMP-0001"},{"in":"query","name":"status","required":false,"schema":{"type":"string","enum":["planning","in_progress","completed","cancelled"]},"description":"Filter by review cycle status (exact match)","example":"in_progress"}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/hris.ReviewCycle"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page"},"total":{"type":"integer","description":"Total documents in the collection"}}}}}}},"tags":["hris"]},"post":{"summary":"Open a performance review cycle","description":"Starts a review for an employee in a period, with a reviewer and due date. It schedules the process; it does not itself record a rating narrative.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.ReviewCycle"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.ReviewCycle"}}}}},"tags":["hris"]}},"/purple-suite/hris/review-cycles/{id}":{"get":{"summary":"Get a performance review cycle by id","description":"Returns one review's period, reviewer, due date, status and overall rating if recorded.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.ReviewCycle"}}}},"404":{"description":"Not found"}},"tags":["hris"]},"patch":{"summary":"Progress or complete a review cycle","description":"Partial update - the way a review moves from planning to in_progress to completed and an overall rating is recorded.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.ReviewCycle"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.ReviewCycle"}}}},"404":{"description":"Not found"}},"tags":["hris"]},"delete":{"summary":"Delete a performance review cycle","description":"DESTRUCTIVE and irreversible: removes the review record and its rating. Prefer setting status to `cancelled`.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"id":{"type":"string"}}}}}},"404":{"description":"Not found"}},"tags":["hris"]}},"/purple-suite/hris/time-off-requests":{"get":{"summary":"List time-off (PTO) requests","description":"A time-off request is one employee's absence request - type (vacation / sick / personal / bereavement / jury_duty), date range, day count and a status of pending / approved / denied / cancelled. Use it for 'who is out' and approval queues; the remaining day balance lives on the employee's pto_balance. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, ... because $ is not a legal tool-input key).","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Max items per page (default 50)"},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Pass the previous response's nextCursor to fetch the next page"},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."},{"in":"query","name":"employee_id","required":false,"schema":{"type":"string"},"description":"Filter by employee ID (exact match)","example":"EMP-0001"},{"in":"query","name":"status","required":false,"schema":{"type":"string","enum":["pending","approved","denied","cancelled"]},"description":"Filter by request status (exact match)","example":"pending"},{"in":"query","name":"type","required":false,"schema":{"type":"string","enum":["vacation","sick","personal","bereavement","jury_duty"]},"description":"Filter by time-off type (exact match)","example":"vacation"}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/hris.TimeOffRequest"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page"},"total":{"type":"integer","description":"Total documents in the collection"}}}}}}},"tags":["hris"]},"post":{"summary":"Submit a time-off (PTO) request","description":"The employee-facing way to ASK for time off: creates the request in `pending` for an approver to decide via the update operation. It does not deduct the PTO balance - use the pto-balance action for that. Fires the `hris.time_off_request.created` outbound webhook to any configured subscriber.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.TimeOffRequest"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.TimeOffRequest"}}}},"400":{"description":"Missing required field or invalid type/status"},"404":{"description":"Employee not found"}},"tags":["hris"]}},"/purple-suite/hris/time-off-requests/{id}":{"get":{"summary":"Get a time-off (PTO) request by id","description":"Returns one absence request: employee, type, date range, day count, status and reason.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.TimeOffRequest"}}}},"404":{"description":"Not found"}},"tags":["hris"]},"patch":{"summary":"Approve, deny or amend a time-off request","description":"The approver-facing counterpart of submitting: partial update, typically setting `status` to approved, denied or cancelled, and also used to amend dates. Fires the `hris.time_off_request.updated` outbound webhook. Approving does not itself adjust the employee's pto_balance.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial), e.g. { \"status\": \"approved\" }","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.TimeOffRequest"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.TimeOffRequest"}}}},"404":{"description":"Not found"}},"tags":["hris"]}},"/purple-suite/hris/work-authorizations":{"get":{"summary":"List work authorizations (visas / right to work)","description":"A work authorization is one person's immigration status in a country - type (citizen, permanent_resident, h1b, skilled_worker, eu_blue_card, ...), country, expiry date, renewal state and any travel restrictions. Use it for right-to-work, visa-expiry and travel-eligibility questions (it pairs with an expense trip's destination_country). Convenience: pass employee=me (the impersonated/default user) or employee=<email> to scope to one person. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, ... because $ is not a legal tool-input key).","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Max items per page (default 50)"},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Pass the previous response's nextCursor to fetch the next page"},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."},{"in":"query","name":"employee","required":false,"schema":{"type":"string"},"description":"Filter to one employee — an email, or the literal 'me' (resolves to the impersonated/default user).","example":"me"},{"in":"query","name":"status","required":false,"schema":{"type":"string","enum":["valid","expiring_soon","expired","renewal_pending","renewal_filed"]},"description":"Filter by status (exact match)","example":"valid"},{"in":"query","name":"country","required":false,"schema":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"description":"Filter by country (exact match)","example":"US"}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/hris.WorkAuthorization"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page"},"total":{"type":"integer","description":"Total documents in the collection"}}}}}}},"tags":["hris"]},"post":{"summary":"Record a work authorization for an employee","description":"Registers a person's right-to-work status in a country (type, expiry, sponsorship). Types citizen and permanent_resident have no expiry and cannot be renewed.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.WorkAuthorization"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.WorkAuthorization"}}}},"400":{"description":"Validation error"}},"tags":["hris"]}},"/purple-suite/hris/work-authorizations/{id}":{"get":{"summary":"Get a work authorization by id","description":"Returns one immigration record: type, country, expiry date, status, renewal state and travel restrictions.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.WorkAuthorization"}}}},"404":{"description":"Not found"}},"tags":["hris"]},"patch":{"summary":"Edit a work authorization","description":"Partial update of an immigration record (correct the expiry, type or restrictions). To file an extension use the renew action, which applies the state machine and extends the expiry for you.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.WorkAuthorization"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.WorkAuthorization"}}}},"404":{"description":"Not found"}},"tags":["hris"]},"delete":{"summary":"Delete a work authorization","description":"DESTRUCTIVE and irreversible: removes the person's right-to-work record, which can make them appear unauthorized. Confirm with the user before calling.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"id":{"type":"string"}}}}}},"404":{"description":"Not found"}},"tags":["hris"]}},"/purple-suite/hris/work-authorizations/{id}/renew":{"post":{"summary":"File a renewal / extension for a work authorization","description":"The action to take when a visa is expiring: transitions an expiring_soon / expired / renewal_pending authorization to renewal_filed, pushes expiry_date out by `extend_months` (default 24) and clears travel restrictions that were tied to the pending renewal. Rejected with 422 for citizen / permanent_resident (no expiry) or when a renewal has already been filed. Use this rather than PATCHing the expiry by hand.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"extend_months":{"type":"integer","default":24,"description":"Months to extend expiry_date by (default 24)."},"notes":{"type":"string","example":"Extension petition filed."}}}}}},"responses":{"200":{"description":"Renewal filed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.WorkAuthorization"}}}},"404":{"description":"Authorization not found"},"422":{"description":"Authorization cannot be renewed (no expiry, or already filed)"}},"tags":["hris"]}},"/purple-suite/hris/job-families":{"get":{"summary":"List job families (career groupings)","description":"A job family is the top of the HRIS job architecture - a career grouping such as Engineering or Finance that job profiles and job roles hang off. Reference catalog, not people: use employees for who holds a job. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, ... because $ is not a legal tool-input key).","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Max items per page (default 50)"},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Pass the previous response's nextCursor to fetch the next page"},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/hris.JobFamily"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page"},"total":{"type":"integer","description":"Total documents in the collection"}}}}}}},"tags":["hris"]},"post":{"summary":"Create a job family","description":"Adds a career grouping to the job architecture. The id is server-derived from the name; duplicates are rejected with 409.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.JobFamily"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.JobFamily"}}}},"400":{"description":"Validation / missing reference"},"409":{"description":"Duplicate"}},"tags":["hris"]}},"/purple-suite/hris/job-families/{id}":{"get":{"summary":"Get a job family by id","description":"Returns one career grouping's name and description.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.JobFamily"}}}},"404":{"description":"Not found"}},"tags":["hris"]},"patch":{"summary":"Rename or edit a job family","description":"Partial update of a career grouping. Renaming CASCADES to the job profiles and job roles that reference it by name.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.JobFamily"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.JobFamily"}}}},"400":{"description":"Validation / missing reference"},"404":{"description":"Not found"}},"tags":["hris"]},"delete":{"summary":"Delete a job family","description":"DESTRUCTIVE: removes the career grouping. Blocked with 409 while job profiles or job roles still reference it, so clear dependents first.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"id":{"type":"string"}}}}}},"404":{"description":"Not found"},"409":{"description":"Blocked — still referenced by dependents"}},"tags":["hris"]}},"/purple-suite/hris/job-levels":{"get":{"summary":"List job levels (leveling framework grades)","description":"A job level is one grade in the leveling framework - a code such as IC3 or M2 with its track (IC or M), numeric tier and management level. Use it to compare seniority or to resolve the job_level on a job profile. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, ... because $ is not a legal tool-input key).","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Max items per page (default 50)"},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Pass the previous response's nextCursor to fetch the next page"},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/hris.JobLevel"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page"},"total":{"type":"integer","description":"Total documents in the collection"}}}}}}},"tags":["hris"]},"post":{"summary":"Create a job level","description":"Adds a grade to the leveling framework (code, track, tier). Reference-catalog administration; it does not re-level any employee.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.JobLevel"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.JobLevel"}}}},"400":{"description":"Validation / missing reference"},"409":{"description":"Duplicate"}},"tags":["hris"]}},"/purple-suite/hris/job-levels/{id}":{"get":{"summary":"Get a job level by id","description":"Returns one grade: its code, display name, track, tier and management level.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.JobLevel"}}}},"404":{"description":"Not found"}},"tags":["hris"]},"patch":{"summary":"Rename or edit a job level","description":"Partial update of a grade's name, track or tier. Renaming CASCADES to job profiles that reference the level code.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.JobLevel"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.JobLevel"}}}},"400":{"description":"Validation / missing reference"},"404":{"description":"Not found"}},"tags":["hris"]},"delete":{"summary":"Delete a job level","description":"DESTRUCTIVE: removes the grade from the framework. Blocked with 409 while job profiles still reference it.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"id":{"type":"string"}}}}}},"404":{"description":"Not found"},"409":{"description":"Blocked — still referenced by dependents"}},"tags":["hris"]}},"/purple-suite/hris/job-profiles":{"get":{"summary":"List job profiles (role templates: title + family + level)","description":"A job profile is the template that ties a TITLE to a job family and a job level (e.g. Software Engineer / Engineering / IC3). It is the canonical answer to 'what titles exist and where do they sit' - employees carry the title, this defines it. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, ... because $ is not a legal tool-input key).","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Max items per page (default 50)"},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Pass the previous response's nextCursor to fetch the next page"},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/hris.JobProfile"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page"},"total":{"type":"integer","description":"Total documents in the collection"}}}}}}},"tags":["hris"]},"post":{"summary":"Create a job profile","description":"Defines a new titled role template against an existing job family and job level. Missing references are rejected with 400.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.JobProfile"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.JobProfile"}}}},"400":{"description":"Validation / missing reference"},"409":{"description":"Duplicate"}},"tags":["hris"]}},"/purple-suite/hris/job-profiles/{id}":{"get":{"summary":"Get a job profile by id","description":"Returns one role template: its title, job family, job level and management level.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.JobProfile"}}}},"404":{"description":"Not found"}},"tags":["hris"]},"patch":{"summary":"Edit a job profile","description":"Partial update of a role template's title, family or level. Renaming CASCADES to job roles that reference this profile.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.JobProfile"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.JobProfile"}}}},"400":{"description":"Validation / missing reference"},"404":{"description":"Not found"}},"tags":["hris"]},"delete":{"summary":"Delete a job profile","description":"DESTRUCTIVE: removes the role template. Blocked with 409 while job roles still reference it.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"id":{"type":"string"}}}}}},"404":{"description":"Not found"},"409":{"description":"Blocked — still referenced by dependents"}},"tags":["hris"]}},"/purple-suite/hris/job-roles":{"get":{"summary":"List job roles (positions under a family)","description":"A job role is a named position under a job family, optionally mapped to a job profile (modeled on ServiceNow sn_hr_core_position). It is the most concrete layer of the job architecture: family -> profile -> role. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, ... because $ is not a legal tool-input key).","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Max items per page (default 50)"},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Pass the previous response's nextCursor to fetch the next page"},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/hris.JobRole"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page"},"total":{"type":"integer","description":"Total documents in the collection"}}}}}}},"tags":["hris"]},"post":{"summary":"Create a job role","description":"Adds a position under an existing job family and, optionally, a job profile. Missing references are rejected with 400.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.JobRole"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.JobRole"}}}},"400":{"description":"Validation / missing reference"},"409":{"description":"Duplicate"}},"tags":["hris"]}},"/purple-suite/hris/job-roles/{id}":{"get":{"summary":"Get a job role by id","description":"Returns one position: its name, description, active flag, job family and mapped job profile.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.JobRole"}}}},"404":{"description":"Not found"}},"tags":["hris"]},"patch":{"summary":"Edit a job role","description":"Partial update of a position's name, description, family/profile mapping or active flag.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.JobRole"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/hris.JobRole"}}}},"400":{"description":"Validation / missing reference"},"404":{"description":"Not found"}},"tags":["hris"]},"delete":{"summary":"Delete a job role","description":"DESTRUCTIVE: removes the position from the catalog. Blocked with 409 while dependents still reference it; prefer setting `active` to false to retire a role.","parameters":[{"$ref":"#/components/parameters/hris.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"id":{"type":"string"}}}}}},"404":{"description":"Not found"},"409":{"description":"Blocked — still referenced by dependents"}},"tags":["hris"]}},"/purple-suite/identity/people":{"get":{"summary":"List people in the Purple Identity directory","parameters":[{"$ref":"#/components/parameters/identity.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/identity.Person"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"description":"The canonical internal roster for this Purple Suite instance — every other app (HRIS, ITSM, Expense, Calendar, PM, …) draws its employees, owners and assignees from these same records, so an email means the same human everywhere. Returns flat Person records (email, first/last/full name, title, department, managerEmail, country, city, timezone, startDate, workStatus). Supports server-side OData querying — $filter, $search, $orderby, $select, $top, $skip, $count (MCP clients see these as _filter, _search, _orderby, _select, _top, _skip, _count because $ is not a legal tool-input key). Example: $filter=department eq 'Engineering' and country eq 'US'. Use this to resolve a person, look up a manager, or build an org view; use identity__list__users only when you need the same people in the Moveworks Identity Gateway wire shape.","tags":["identity"]},"post":{"summary":"Add a person to the Purple Identity directory","parameters":[{"$ref":"#/components/parameters/identity.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/identity.Person"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/identity.Person"}}}}},"description":"Creates a Person in the instance's canonical internal roster, making that email resolvable across every other Purple app. Use only for internal staff (employees, contingent workers, interns) — external parties such as CRM contacts, ATS candidates or bank customers live in their own app and must not be added here.","tags":["identity"]}},"/purple-suite/identity/people/{id}":{"get":{"summary":"Get one person from the Purple Identity directory","parameters":[{"$ref":"#/components/parameters/identity.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/identity.Person"}}}},"404":{"description":"Not found"}},"description":"Fetches a single Person by directory id (e.g. PERSON-0001) from the canonical internal roster. If you only have an email, call identity__list__people with $filter=email eq 'someone@acme.com' instead. Returns the flat Person shape; identity__get__users returns the same human in the nested Identity Gateway shape.","tags":["identity"]},"patch":{"summary":"Update a person's directory record","description":"Partially updates a Person in the canonical roster (e.g. title, department, managerEmail, location, workStatus). Because every other Purple app reads people from here, the change is immediately visible suite-wide. Send only the fields you want to change.","parameters":[{"$ref":"#/components/parameters/identity.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/identity.Person"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/identity.Person"}}}},"404":{"description":"Not found"}},"tags":["identity"]},"delete":{"summary":"Delete a person from the Purple Identity directory","parameters":[{"$ref":"#/components/parameters/identity.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"description":"DESTRUCTIVE and irreversible: permanently removes the Person from the canonical roster, so their email no longer resolves in any Purple app and existing references to them (tickets, expenses, enrollments) are left dangling. For a departure, prefer identity__update__people to change their status rather than deleting the record.","tags":["identity"]}},"/purple-suite/identity/users":{"get":{"x-odata":false,"summary":"List users in the Moveworks Identity Gateway shape","description":"Emulates the Moveworks Identity Gateway 'List Users' contract for user-directory sync. These are the SAME humans as identity__list__people, projected into the gateway's nested envelope ({ results: [{ user, system_identity, last_updated_at }], next_page_token }) — reach for it when you are exercising or debugging a Moveworks identity sync, and for everything else use identity__list__people, which returns the flat, easier-to-read Person records. This endpoint is NOT OData: page with pageToken/pageSize and narrow results with the gateway's own `filter` expression (e.g. filter=department eq 'Engineering'), not $filter/$top.","parameters":[{"$ref":"#/components/parameters/identity.InstanceId"},{"in":"query","name":"pageToken","schema":{"type":"string"},"description":"Opaque paging token — pass the previous response's next_page_token."},{"in":"query","name":"pageSize","schema":{"type":"integer","default":1000},"description":"Maximum users to return in one page (default 1000)."},{"in":"query","name":"filter","schema":{"type":"string"},"description":"Filter expression over person fields. Operators eq/ne/gt/lt, combined with and/or and parentheses. Example: \"department eq 'Engineering'\"."}],"responses":{"200":{"description":"A page of users","content":{"application/json":{"schema":{"type":"object","properties":{"results":{"type":"array","items":{"$ref":"#/components/schemas/identity.IdentityGatewayUser"}},"next_page_token":{"type":"string","description":"Token for the next page, or empty on the last page."}}}}}},"400":{"description":"Invalid filter expression"}},"tags":["identity"]}},"/purple-suite/identity/users/{userId}":{"get":{"x-odata":false,"summary":"Get one user in the Moveworks Identity Gateway shape","description":"Emulates the Moveworks Identity Gateway 'Get User by ID' contract. userId is the user's system_identity.id, which is the same value as the directory's person id (e.g. PERSON-0001). Returns one nested gateway user record with no results envelope. Prefer identity__get__people unless you specifically need the gateway wire shape.","parameters":[{"$ref":"#/components/parameters/identity.InstanceId"},{"in":"path","name":"userId","required":true,"schema":{"type":"string"},"description":"The user's system_identity.id (e.g. PERSON-0001)."}],"responses":{"200":{"description":"The user","content":{"application/json":{"schema":{"$ref":"#/components/schemas/identity.IdentityGatewayUser"}}}},"404":{"description":"User not found"}},"tags":["identity"]}},"/purple-suite/itsm/incidents":{"get":{"summary":"List IT incidents (unplanned break/fix tickets)","parameters":[{"$ref":"#/components/parameters/itsm.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/itsm.Incident"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"description":"Incidents in Purple ITSM (a ServiceNow-style IT service desk) are unplanned disruptions someone reported — VPN down, laptop broken — with status (open | in_progress | resolved | closed | cancelled), priority P1–P4, category, assignee, reporter and escalation_tier. Distinct from service requests (asking for something), problems (root cause behind repeat incidents) and alerts (raised by monitoring). Supports server-side OData querying — $filter, $search, $orderby, $select, $top, $skip, $count (MCP clients see these as _filter, _search, _orderby, _select, _top, _skip, _count because $ is not a legal tool-input key). Example: $filter=status eq 'open' and priority eq 'P1'&$orderby=createdAt desc.","tags":["itsm"]},"post":{"summary":"Open a new IT incident ticket","parameters":[{"$ref":"#/components/parameters/itsm.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/itsm.Incident"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/itsm.Incident"}}}}},"description":"Files an incident on the IT service desk — title, priority (P1–P4), category (hardware/software/network/access/other), reporter and assignee. Use this when something is broken; if the user is asking to be GIVEN something (laptop, access, software) create a service request instead.","tags":["itsm"]}},"/purple-suite/itsm/incidents/{id}":{"get":{"summary":"Get one IT incident by ticket number","parameters":[{"$ref":"#/components/parameters/itsm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/itsm.Incident"}}}},"404":{"description":"Not found"}},"description":"Fetches a single incident by id (e.g. INC-0001) with its current status, priority, assignee, escalation tier and timestamps. Use it to answer 'what's the status of my ticket'; search by other fields with itsm__list__incidents.","tags":["itsm"]},"patch":{"summary":"Update an IT incident (reassign, reprioritise, resolve)","description":"The general-purpose incident write: change assignee, priority, category, notes or status. State-machine rules are enforced — valid statuses are open | in_progress | resolved | closed | cancelled; an open incident must pass through in_progress before it can be closed; and reopening a resolved/closed incident requires reopen_reason in the body. To raise the escalation tier use itsm__escalate__incidents, which is the only way that counter moves.","parameters":[{"$ref":"#/components/parameters/itsm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/itsm.Incident"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/itsm.Incident"}}}},"404":{"description":"Not found"}},"tags":["itsm"]},"delete":{"summary":"Delete an IT incident record","parameters":[{"$ref":"#/components/parameters/itsm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"description":"DESTRUCTIVE and irreversible: erases the incident and its history from the service desk entirely. This is not how tickets are normally finished — to end a ticket set its status to resolved/closed/cancelled with itsm__update__incidents; delete only for records created in error.","tags":["itsm"]}},"/purple-suite/itsm/incidents/{id}/escalate":{"post":{"summary":"Escalate an incident to the next support tier","description":"The only way the escalation counter moves: bumps the incident's escalation_tier by 1 (tier 3 is the ceiling — a further call returns 400), stamps escalated_at and stores optional escalation_notes. Use it when a ticket needs senior/urgent attention; use itsm__update__incidents to merely change priority, assignee or status. Fails with 422 on a resolved or closed incident.","parameters":[{"$ref":"#/components/parameters/itsm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"escalation_notes":{"type":"string","example":"Customer impact escalating; looping in senior support"}}}}}},"responses":{"200":{"description":"Updated incident after escalation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/itsm.Incident"}}}},"400":{"description":"Incident already at maximum escalation tier (3)"},"404":{"description":"Not found"},"422":{"description":"Cannot escalate a resolved or closed incident"}},"tags":["itsm"]}},"/purple-suite/itsm/changes":{"get":{"summary":"List change requests (planned IT changes)","parameters":[{"$ref":"#/components/parameters/itsm.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/itsm.Change"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"description":"A Change is a PLANNED modification to the IT estate going through change management — OS upgrade, firewall rule, release — with status (draft | pending_approval | approved | in_progress | implemented | completed | rejected | cancelled), riskLevel, scheduledDate, approver and a cab_approval record. Contrast with incidents, which are unplanned breakages. Supports server-side OData querying — $filter, $search, $orderby, $select, $top, $skip, $count (MCP clients see these as _filter, _search, _orderby, _select, _top, _skip, _count because $ is not a legal tool-input key). Example: $filter=status eq 'pending_approval' and riskLevel eq 'high'.","tags":["itsm"]},"post":{"summary":"Raise a change request","parameters":[{"$ref":"#/components/parameters/itsm.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/itsm.Change"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/itsm.Change"}}}}},"description":"Creates a change request for planned work — title, riskLevel, scheduledDate and approver — normally starting in draft or pending_approval. Approval itself is not granted here: that happens through itsm__cab_approve__changes.","tags":["itsm"]}},"/purple-suite/itsm/changes/{id}":{"get":{"summary":"Get one change request","parameters":[{"$ref":"#/components/parameters/itsm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/itsm.Change"}}}},"404":{"description":"Not found"}},"description":"Fetches a single change by id (e.g. CHG-0001) with its status, risk, schedule and the cab_approval block recording who approved or rejected it and why.","tags":["itsm"]},"patch":{"summary":"Update a change request's details or status","description":"Edits change fields — title, riskLevel, scheduledDate, approver — and moves it through non-approval statuses such as in_progress, implemented, completed or cancelled. Do NOT use this to approve or reject: itsm__cab_approve__changes and itsm__cab_reject__changes are the CAB decisions and they also write the auditable cab_approval record.","parameters":[{"$ref":"#/components/parameters/itsm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/itsm.Change"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/itsm.Change"}}}},"404":{"description":"Not found"}},"tags":["itsm"]},"delete":{"summary":"Delete a change request","parameters":[{"$ref":"#/components/parameters/itsm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"description":"DESTRUCTIVE and irreversible: removes the change request and its CAB approval trail. To call off planned work while keeping the audit history, set status to 'cancelled' with itsm__update__changes instead.","tags":["itsm"]}},"/purple-suite/itsm/changes/{id}/cab-approve":{"post":{"summary":"Approve a change at the Change Advisory Board","description":"Records the formal CAB decision to approve planned work: writes approved_by/approved_at into the change's auditable cab_approval block and sets status to 'approved'. This is the governance action — do not simulate it by patching status with itsm__update__changes, which leaves no approval record. Fails with 422 if the change is already approved, rejected or cancelled.","parameters":[{"$ref":"#/components/parameters/itsm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"approved_by":{"type":"string","example":"manager1@acme.com"}},"required":["approved_by"]}}}},"responses":{"200":{"description":"Updated change after CAB approval","content":{"application/json":{"schema":{"$ref":"#/components/schemas/itsm.Change"}}}},"404":{"description":"Not found"},"422":{"description":"Change cannot be approved in its current state"}},"tags":["itsm"]}},"/purple-suite/itsm/changes/{id}/cab-reject":{"post":{"summary":"Reject a change at the Change Advisory Board","description":"Records the formal CAB decision to refuse planned work: requires rejected_by and a reason, writes them into the change's cab_approval block and sets status to 'rejected'. The counterpart to itsm__cab_approve__changes; prefer it over patching status directly so the refusal is auditable. Fails with 422 if the change is already rejected or implemented.","parameters":[{"$ref":"#/components/parameters/itsm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"rejected_by":{"type":"string","example":"manager2@acme.com"},"reason":{"type":"string","example":"Insufficient testing evidence provided"}},"required":["rejected_by","reason"]}}}},"responses":{"200":{"description":"Updated change after CAB rejection","content":{"application/json":{"schema":{"$ref":"#/components/schemas/itsm.Change"}}}},"404":{"description":"Not found"},"422":{"description":"Change cannot be rejected in its current state"}},"tags":["itsm"]}},"/purple-suite/itsm/sla_policies":{"get":{"summary":"List SLA policies (response/resolution targets)","parameters":[{"$ref":"#/components/parameters/itsm.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/itsm.SlaPolicy"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"description":"SLA policies are the service-desk's configured time targets, not tickets: each names a priority (P1–P4) with responseTimeHours and resolutionTimeHours, plus active and paused flags. Use it to answer 'what is the SLA for a P1?' or to audit which policies are currently paused. Supports server-side OData querying — $filter, $search, $orderby, $select, $top, $skip, $count (MCP clients see these as _filter, _search, _orderby, _select, _top, _skip, _count because $ is not a legal tool-input key). Example: $filter=paused eq true.","tags":["itsm"]},"post":{"summary":"Create an SLA policy","parameters":[{"$ref":"#/components/parameters/itsm.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/itsm.SlaPolicy"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/itsm.SlaPolicy"}}}}},"description":"Defines a new service-level target — name, the priority it applies to, responseTimeHours and resolutionTimeHours. Configuration only; it does not alter any existing incident.","tags":["itsm"]}},"/purple-suite/itsm/sla_policies/{id}":{"get":{"summary":"Get one SLA policy","parameters":[{"$ref":"#/components/parameters/itsm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/itsm.SlaPolicy"}}}},"404":{"description":"Not found"}},"description":"Fetches a single SLA policy by id (e.g. SLA-001) with its response/resolution targets and its active and paused flags.","tags":["itsm"]},"patch":{"summary":"Update an SLA policy's targets","description":"Edits a policy's name, priority, response/resolution hours or active flag. For the specific act of suspending or restarting SLA timing use itsm__pause__sla_policies / itsm__resume__sla_policies, which enforce the paused-state transition.","parameters":[{"$ref":"#/components/parameters/itsm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/itsm.SlaPolicy"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/itsm.SlaPolicy"}}}},"404":{"description":"Not found"}},"tags":["itsm"]},"delete":{"summary":"Delete an SLA policy","parameters":[{"$ref":"#/components/parameters/itsm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"description":"DESTRUCTIVE and irreversible: removes the SLA definition, so tickets at that priority no longer have a target. To stop the clock temporarily use itsm__pause__sla_policies instead.","tags":["itsm"]}},"/purple-suite/itsm/sla-policies/{id}/pause":{"post":{"summary":"Pause an SLA policy (stop the clock)","description":"Suspends a service-level target by setting paused=true, e.g. during a maintenance window so response/resolution clocks do not count against IT. Reverse it with itsm__resume__sla_policies; use itsm__update__sla_policies to change the target hours themselves. Fails with 422 if the policy is already paused.","parameters":[{"$ref":"#/components/parameters/itsm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Updated SLA policy after pause","content":{"application/json":{"schema":{"$ref":"#/components/schemas/itsm.SlaPolicy"}}}},"404":{"description":"Not found"},"422":{"description":"SLA policy is already paused"}},"tags":["itsm"]}},"/purple-suite/itsm/sla-policies/{id}/resume":{"post":{"summary":"Resume a paused SLA policy (restart the clock)","description":"Puts a suspended service-level target back in force by setting paused=false — the counterpart to itsm__pause__sla_policies, typically once a maintenance window ends. Fails with 422 if the policy is not currently paused.","parameters":[{"$ref":"#/components/parameters/itsm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Updated SLA policy after resume","content":{"application/json":{"schema":{"$ref":"#/components/schemas/itsm.SlaPolicy"}}}},"404":{"description":"Not found"},"422":{"description":"SLA policy is not paused and cannot be resumed"}},"tags":["itsm"]}},"/purple-suite/itsm/problems":{"get":{"summary":"List problems (root causes behind repeat incidents)","parameters":[{"$ref":"#/components/parameters/itsm.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/itsm.Problem"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"description":"ITIL problem records: the underlying cause investigated behind a pattern of incidents (e.g. 'recurring login failures'), with status open | known_error | resolved and a rootCause classification. Use this for root-cause / known-error questions; individual user-reported breakages are itsm__list__incidents. Supports server-side OData querying — $filter, $search, $orderby, $select, $top, $skip, $count (MCP clients see these as _filter, _search, _orderby, _select, _top, _skip, _count because $ is not a legal tool-input key). Example: $filter=status eq 'known_error'.","tags":["itsm"]},"post":{"summary":"Open a problem investigation","parameters":[{"$ref":"#/components/parameters/itsm.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/itsm.Problem"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/itsm.Problem"}}}}},"description":"Creates a problem record to investigate the root cause behind repeating incidents. Use it when the same failure keeps recurring; log the individual user-facing outages as incidents.","tags":["itsm"]}},"/purple-suite/itsm/problems/{id}":{"get":{"summary":"Get one problem record","parameters":[{"$ref":"#/components/parameters/itsm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/itsm.Problem"}}}},"404":{"description":"Not found"}},"description":"Fetches a single problem by id (e.g. PRB-0001) with its status and rootCause classification — useful for checking whether a recurring failure is already a known error.","tags":["itsm"]},"patch":{"summary":"Update a problem investigation","description":"Edits the problem's title, status (open | known_error | resolved) or rootCause as the investigation progresses — for example marking it a known error once the cause is identified.","parameters":[{"$ref":"#/components/parameters/itsm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/itsm.Problem"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/itsm.Problem"}}}},"404":{"description":"Not found"}},"tags":["itsm"]},"delete":{"summary":"Delete a problem record","parameters":[{"$ref":"#/components/parameters/itsm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"description":"DESTRUCTIVE and irreversible: erases the problem record and its root-cause findings. Normally close an investigation by setting status to 'resolved' with itsm__update__problems instead.","tags":["itsm"]}},"/purple-suite/itsm/service_requests":{"get":{"summary":"List service requests (asks from the service catalog)","parameters":[{"$ref":"#/components/parameters/itsm.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/itsm.ServiceRequest"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"description":"Service requests are people ASKING for something standard — a new laptop, software, access — with status submitted | approved | in_progress | completed | cancelled, requestedBy and a fulfillmentGroup (IT Support, Security, Procurement, Facilities). Use itsm__list__incidents instead when something is broken. Supports server-side OData querying — $filter, $search, $orderby, $select, $top, $skip, $count (MCP clients see these as _filter, _search, _orderby, _select, _top, _skip, _count because $ is not a legal tool-input key). Example: $filter=status eq 'submitted' and fulfillmentGroup eq 'Procurement'.","tags":["itsm"]},"post":{"summary":"Submit a service request","parameters":[{"$ref":"#/components/parameters/itsm.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/itsm.ServiceRequest"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/itsm.ServiceRequest"}}}}},"description":"Raises a catalog request on behalf of someone — title, requestedBy and the fulfillmentGroup that will handle it — normally starting in 'submitted'. For a fault or outage file an incident instead.","tags":["itsm"]}},"/purple-suite/itsm/service_requests/{id}":{"get":{"summary":"Get one service request","parameters":[{"$ref":"#/components/parameters/itsm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/itsm.ServiceRequest"}}}},"404":{"description":"Not found"}},"description":"Fetches a single service request by id (e.g. SR-0001) with its status, requester and fulfillment group — the tool for 'where is my laptop request?'.","tags":["itsm"]},"patch":{"summary":"Update or progress a service request","description":"Edits the request or moves it along its lifecycle by setting status to approved, in_progress, completed or cancelled, and can reroute it to a different fulfillmentGroup.","parameters":[{"$ref":"#/components/parameters/itsm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/itsm.ServiceRequest"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/itsm.ServiceRequest"}}}},"404":{"description":"Not found"}},"tags":["itsm"]},"delete":{"summary":"Delete a service request","parameters":[{"$ref":"#/components/parameters/itsm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"description":"DESTRUCTIVE and irreversible: removes the request and its history. To withdraw a request while keeping the record, set status to 'cancelled' with itsm__update__service_requests instead.","tags":["itsm"]}},"/purple-suite/itsm/cis":{"get":{"summary":"List CMDB configuration items (IT assets)","parameters":[{"$ref":"#/components/parameters/itsm.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/itsm.ConfigurationItem"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"description":"The CMDB inventory of managed IT assets — servers, workstations, network devices, applications, databases, storage — each with name, type, status (operational | maintenance | retired | ordered), environment (production/staging/development/dr) and owning team. Use it to find the asset behind a ticket or audit what is in production. Supports server-side OData querying — $filter, $search, $orderby, $select, $top, $skip, $count (MCP clients see these as _filter, _search, _orderby, _select, _top, _skip, _count because $ is not a legal tool-input key). Example: $filter=type eq 'server' and environment eq 'production'.","tags":["itsm"]},"post":{"summary":"Add a configuration item to the CMDB","parameters":[{"$ref":"#/components/parameters/itsm.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/itsm.ConfigurationItem"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/itsm.ConfigurationItem"}}}}},"description":"Registers a new managed asset in the CMDB — name, type, environment, owning team and lifecycle status. Inventory only; it does not create any ticket.","tags":["itsm"]}},"/purple-suite/itsm/cis/{id}":{"get":{"summary":"Get one CMDB configuration item","parameters":[{"$ref":"#/components/parameters/itsm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/itsm.ConfigurationItem"}}}},"404":{"description":"Not found"}},"description":"Fetches a single configuration item by id (e.g. CI-a1b2c3d4) with its type, environment, owner and operational status.","tags":["itsm"]},"patch":{"summary":"Update a CMDB configuration item","description":"Edits an asset record — status (e.g. move it to maintenance or retired), environment, owner or name. Updates inventory metadata only; it does not schedule the work, which belongs in a change request.","parameters":[{"$ref":"#/components/parameters/itsm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/itsm.ConfigurationItem"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/itsm.ConfigurationItem"}}}},"404":{"description":"Not found"}},"tags":["itsm"]},"delete":{"summary":"Delete a CMDB configuration item","parameters":[{"$ref":"#/components/parameters/itsm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"description":"DESTRUCTIVE and irreversible: removes the asset from the CMDB, breaking references from tickets and changes. For decommissioned kit set status to 'retired' with itsm__update__cis instead.","tags":["itsm"]}},"/purple-suite/itsm/kb_articles":{"get":{"summary":"Search the IT knowledge base","parameters":[{"$ref":"#/components/parameters/itsm.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/itsm.KbArticle"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"description":"Self-service how-to and troubleshooting articles published by IT (title, category, author, publishedAt, views, helpful counts). Reach for this FIRST when a user asks how to do something, before opening a ticket. Supports server-side OData querying — $filter, $search, $orderby, $select, $top, $skip, $count (MCP clients see these as _filter, _search, _orderby, _select, _top, _skip, _count because $ is not a legal tool-input key). Example: $search=password reset, or $filter=category eq 'Authentication'&$orderby=helpful desc.","tags":["itsm"]},"post":{"summary":"Publish a knowledge base article","parameters":[{"$ref":"#/components/parameters/itsm.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/itsm.KbArticle"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/itsm.KbArticle"}}}}},"description":"Creates a new KB article — title, category (Authentication, Network, Hardware, Software, Security, Onboarding), author and publish date — so users can self-serve instead of raising incidents.","tags":["itsm"]}},"/purple-suite/itsm/kb_articles/{id}":{"get":{"summary":"Get one knowledge base article","parameters":[{"$ref":"#/components/parameters/itsm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/itsm.KbArticle"}}}},"404":{"description":"Not found"}},"description":"Fetches a single KB article by id (e.g. KB-0001) with its category, author and usefulness stats — use it to cite specific guidance once you have found it via itsm__list__kb_articles.","tags":["itsm"]},"patch":{"summary":"Update a knowledge base article","description":"Edits an existing article's title, category, author or publication date — for example to correct out-of-date guidance rather than publishing a duplicate.","parameters":[{"$ref":"#/components/parameters/itsm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/itsm.KbArticle"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/itsm.KbArticle"}}}},"404":{"description":"Not found"}},"tags":["itsm"]},"delete":{"summary":"Delete a knowledge base article","parameters":[{"$ref":"#/components/parameters/itsm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"description":"DESTRUCTIVE and irreversible: unpublishes and erases the article, so users searching the knowledge base will no longer find that guidance.","tags":["itsm"]}},"/purple-suite/itsm/alerts":{"get":{"summary":"List monitoring alerts from observability tools","parameters":[{"$ref":"#/components/parameters/itsm.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/itsm.Alert"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"description":"Machine-generated alerts fired by monitoring systems (Datadog, PagerDuty, Nagios, CloudWatch, Prometheus) with severity critical | warning | info, status active | acknowledged | resolved and trigger/resolve times. These are detections, not human-reported tickets — use itsm__list__incidents for those. Supports server-side OData querying — $filter, $search, $orderby, $select, $top, $skip, $count (MCP clients see these as _filter, _search, _orderby, _select, _top, _skip, _count because $ is not a legal tool-input key). Example: $filter=status eq 'active' and severity eq 'critical'.","tags":["itsm"]},"post":{"summary":"Record a monitoring alert","parameters":[{"$ref":"#/components/parameters/itsm.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/itsm.Alert"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/itsm.Alert"}}}}},"description":"Creates an alert record as if a monitoring tool had fired it — title, severity, source system and triggeredAt. Mostly for simulating observability signals; a person reporting an issue should be filed as an incident.","tags":["itsm"]}},"/purple-suite/itsm/alerts/{id}":{"get":{"summary":"Get one monitoring alert","parameters":[{"$ref":"#/components/parameters/itsm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/itsm.Alert"}}}},"404":{"description":"Not found"}},"description":"Fetches a single alert by id (e.g. ALT-0001) with its severity, source, trigger time and whether it has been acknowledged or resolved.","tags":["itsm"]},"patch":{"summary":"Acknowledge or resolve a monitoring alert","description":"Updates an alert — most often setting status to 'acknowledged' when someone picks it up or 'resolved' once the condition clears, and stamping resolvedAt.","parameters":[{"$ref":"#/components/parameters/itsm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/itsm.Alert"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/itsm.Alert"}}}},"404":{"description":"Not found"}},"tags":["itsm"]},"delete":{"summary":"Delete a monitoring alert","parameters":[{"$ref":"#/components/parameters/itsm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"description":"DESTRUCTIVE and irreversible: erases the alert record and its detection history. To clear noise while keeping the trail, set its status to 'resolved' with itsm__update__alerts instead.","tags":["itsm"]}},"/purple-suite/lms/courses":{"get":{"summary":"List courses in the Purple Learn catalog","parameters":[{"$ref":"#/components/parameters/lms.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/lms.Course"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"description":"Browse the learning catalog of this instance (Cornerstone/Docebo-style): each Course has a title, level, format (video/article/path), duration and skills_taught. This is the catalog of available training, NOT anyone's progress — for who is taking what, use lms__list__enrollments. Supports server-side OData querying — $filter, $search, $orderby, $select, $top, $skip, $count (MCP clients see these as _filter, _search, _orderby, _select, _top, _skip, _count because $ is not a legal tool-input key). Example: $filter=level eq 'beginner' and $search=leadership.","tags":["lms"]},"post":{"summary":"Add a course to the Purple Learn catalog","parameters":[{"$ref":"#/components/parameters/lms.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/lms.Course"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/lms.Course"}}}}},"description":"Creates a new catalog entry (title required; optionally level, format, provider, duration_minutes and the skills_taught it develops). Creating a course does not assign it to anyone — use lms__enroll__courses to put a learner on it.","tags":["lms"]}},"/purple-suite/lms/courses/{id}":{"get":{"summary":"Get one Purple Learn course","parameters":[{"$ref":"#/components/parameters/lms.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/lms.Course"}}}},"404":{"description":"Not found"}},"description":"Fetches a single catalog course by id (e.g. CRS-0001), including its skills_taught, level, format and duration. Use it to confirm a course exists or check what skills it covers before enrolling someone.","tags":["lms"]},"patch":{"summary":"Update a Purple Learn course","description":"Partially updates a catalog course (title, description, level, format, provider, duration_minutes, skills_taught). Affects the catalog entry only — existing learner enrollments and their progress are untouched.","parameters":[{"$ref":"#/components/parameters/lms.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/lms.Course"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/lms.Course"}}}},"404":{"description":"Not found"}},"tags":["lms"]},"delete":{"summary":"Delete a Purple Learn course","parameters":[{"$ref":"#/components/parameters/lms.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"description":"DESTRUCTIVE and irreversible: removes the course from the catalog. Enrollment records that point at this course_id are not cleaned up and will reference a missing course, so prefer retiring a course via lms__update__courses unless it was created in error.","tags":["lms"]}},"/purple-suite/lms/courses/{id}/enroll":{"post":{"summary":"Enroll a learner in a course","description":"The normal way to put someone on training: creates a not_started Enrollment for the given learner (identify them by employee_id or user_email) against the course in the path. Fails with 422 if that learner already has an active (not_started/in_progress) enrollment for the course, and 404 if the course does not exist. Prefer this over lms__create__enrollments, which skips those checks.","parameters":[{"$ref":"#/components/parameters/lms.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"employee_id":{"type":"string","description":"Learner's HRS employee ID. Provide this or user_email.","example":"EMP-0001"},"user_email":{"type":"string","format":"email","description":"Learner's email. Provide this or employee_id.","example":"alice.smith0@acme.com"}}}}}},"responses":{"201":{"description":"Enrollment created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/lms.Enrollment"}}}},"400":{"description":"Missing learner (employee_id or user_email)"},"404":{"description":"Course not found"},"422":{"description":"Learner already has an active enrollment for this course"}},"tags":["lms"]}},"/purple-suite/lms/enrollments":{"get":{"summary":"List learner enrollments (training progress)","parameters":[{"$ref":"#/components/parameters/lms.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/lms.Enrollment"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"description":"An Enrollment is one learner's run through one course — course_id, learner (employee_id or user_email), status (not_started | in_progress | completed | dropped), progress_pct, score and completion date. Use this for 'who has completed X', overdue training or compliance reporting; use lms__list__courses for the catalog itself. Supports server-side OData querying — $filter, $search, $orderby, $select, $top, $skip, $count (MCP clients see these as _filter, _search, _orderby, _select, _top, _skip, _count because $ is not a legal tool-input key). Example: $filter=status eq 'completed' and course_id eq 'CRS-0001'.","tags":["lms"]},"post":{"summary":"Create an enrollment record directly","parameters":[{"$ref":"#/components/parameters/lms.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/lms.Enrollment"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/lms.Enrollment"}}}}},"description":"Low-level create that writes an Enrollment exactly as given — useful for backfilling historical or already-completed training. To simply put a learner on a course, prefer lms__enroll__courses, which resolves the learner, defaults the status and blocks duplicate active enrollments; this operation performs none of those checks.","tags":["lms"]}},"/purple-suite/lms/enrollments/{id}":{"get":{"summary":"Get one learner enrollment","parameters":[{"$ref":"#/components/parameters/lms.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/lms.Enrollment"}}}},"404":{"description":"Not found"}},"description":"Fetches a single Enrollment by id (e.g. ENR-0001) with its current status, progress_pct, score and timestamps. Use it to check one learner's progress on one course after finding the id via lms__list__enrollments.","tags":["lms"]},"patch":{"summary":"Update enrollment progress or status","description":"Partially updates an Enrollment — typically progress_pct, score, or status among not_started | in_progress | completed | dropped (use this to drop or resume someone). A completed enrollment cannot be reopened. To finish a course properly, prefer lms__complete__enrollments, which also sets progress to 100 and stamps completed_at.","parameters":[{"$ref":"#/components/parameters/lms.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/lms.Enrollment"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/lms.Enrollment"}}}},"404":{"description":"Not found"}},"tags":["lms"]},"delete":{"summary":"Delete a learner enrollment","parameters":[{"$ref":"#/components/parameters/lms.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"description":"DESTRUCTIVE and irreversible: erases the enrollment and with it the learner's recorded progress, score and completion history for that course. To take someone off a course while keeping the audit trail, set status to 'dropped' via lms__update__enrollments instead.","tags":["lms"]}},"/purple-suite/lms/enrollments/{id}/complete":{"post":{"summary":"Mark an enrollment complete","description":"The correct way to finish training: transitions the enrollment to completed, forces progress_pct to 100, stamps completed_at and stores an optional final score. This is a one-way transition — a completed enrollment can never be reopened — and it fails with 422 if the enrollment is already completed or was dropped. Use lms__update__enrollments for partial progress instead.","parameters":[{"$ref":"#/components/parameters/lms.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"score":{"type":"number","minimum":0,"maximum":100,"description":"Final assessment score, if any.","example":88}}}}}},"responses":{"200":{"description":"Enrollment completed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/lms.Enrollment"}}}},"404":{"description":"Enrollment not found"},"422":{"description":"Enrollment already completed or dropped"}},"tags":["lms"]}},"/purple-suite/manager-experience/team_memberships":{"get":{"summary":"List team_memberships","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/manager-experience.TeamMembership"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["manager-experience"]},"post":{"summary":"Create team_membership","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.TeamMembership"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.TeamMembership"}}}}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/team_memberships/{id}":{"get":{"summary":"Get team_membership","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.TeamMembership"}}}},"404":{"description":"Not found"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/chats":{"get":{"summary":"List chats","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/manager-experience.Chat"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["manager-experience"]},"post":{"summary":"Create chat","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.Chat"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.Chat"}}}}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/chats/{id}":{"get":{"summary":"Get chat","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.Chat"}}}},"404":{"description":"Not found"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/chat_messages":{"get":{"summary":"List chat_messages","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/manager-experience.ChatMessage"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["manager-experience"]},"post":{"summary":"Create chat_message","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.ChatMessage"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.ChatMessage"}}}}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/chat_messages/{id}":{"get":{"summary":"Get chat_message","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.ChatMessage"}}}},"404":{"description":"Not found"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/teams":{"get":{"summary":"List teams","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/manager-experience.Team"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["manager-experience"]},"post":{"summary":"Create team","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.Team"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.Team"}}}}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/teams/{id}":{"get":{"summary":"Get team","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.Team"}}}},"404":{"description":"Not found"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/channels":{"get":{"summary":"List channels","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/manager-experience.Channel"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["manager-experience"]},"post":{"summary":"Create channel","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.Channel"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.Channel"}}}}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/channels/{id}":{"get":{"summary":"Get channel","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.Channel"}}}},"404":{"description":"Not found"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/channel_messages":{"get":{"summary":"List channel_messages","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/manager-experience.ChannelMessage"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["manager-experience"]},"post":{"summary":"Create channel_message","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.ChannelMessage"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.ChannelMessage"}}}}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/channel_messages/{id}":{"get":{"summary":"Get channel_message","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.ChannelMessage"}}}},"404":{"description":"Not found"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/mail_messages":{"get":{"summary":"List mail_messages","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/manager-experience.MailMessage"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["manager-experience"]},"post":{"summary":"Create mail_message","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.MailMessage"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.MailMessage"}}}}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/mail_messages/{id}":{"get":{"summary":"Get mail_message","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.MailMessage"}}}},"404":{"description":"Not found"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/jira_projects":{"get":{"summary":"List jira_projects","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/manager-experience.JiraProject"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["manager-experience"]},"post":{"summary":"Create jira_project","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.JiraProject"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.JiraProject"}}}}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/jira_projects/{id}":{"get":{"summary":"Get jira_project","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.JiraProject"}}}},"404":{"description":"Not found"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/jira_sprints":{"get":{"summary":"List jira_sprints","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/manager-experience.JiraSprint"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["manager-experience"]},"post":{"summary":"Create jira_sprint","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.JiraSprint"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.JiraSprint"}}}}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/jira_sprints/{id}":{"get":{"summary":"Get jira_sprint","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.JiraSprint"}}}},"404":{"description":"Not found"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/jira_issues":{"get":{"summary":"List jira_issues","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/manager-experience.JiraIssue"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["manager-experience"]},"post":{"summary":"Create jira_issue","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.JiraIssue"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.JiraIssue"}}}}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/jira_issues/{id}":{"get":{"summary":"Get jira_issue","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.JiraIssue"}}}},"404":{"description":"Not found"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/stories":{"get":{"summary":"List stories","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/manager-experience.Story"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["manager-experience"]},"post":{"summary":"Create story","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.Story"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.Story"}}}}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/stories/{id}":{"get":{"summary":"Get story","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.Story"}}}},"404":{"description":"Not found"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/defects":{"get":{"summary":"List defects","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/manager-experience.Defect"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["manager-experience"]},"post":{"summary":"Create defect","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.Defect"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.Defect"}}}}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/defects/{id}":{"get":{"summary":"Get defect","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.Defect"}}}},"404":{"description":"Not found"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/resource_allocations":{"get":{"summary":"List resource_allocations","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/manager-experience.ResourceAllocation"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["manager-experience"]},"post":{"summary":"Create resource_allocation","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.ResourceAllocation"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.ResourceAllocation"}}}}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/resource_allocations/{id}":{"get":{"summary":"Get resource_allocation","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.ResourceAllocation"}}}},"404":{"description":"Not found"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/workday_learning_enrollments":{"get":{"summary":"List workday_learning_enrollments","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/manager-experience.WorkdayLearningEnrollment"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["manager-experience"]},"post":{"summary":"Create workday_learning_enrollment","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.WorkdayLearningEnrollment"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.WorkdayLearningEnrollment"}}}}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/workday_learning_enrollments/{id}":{"get":{"summary":"Get workday_learning_enrollment","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.WorkdayLearningEnrollment"}}}},"404":{"description":"Not found"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/workday_certifications":{"get":{"summary":"List workday_certifications","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/manager-experience.WorkdayCertification"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["manager-experience"]},"post":{"summary":"Create workday_certification","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.WorkdayCertification"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.WorkdayCertification"}}}}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/workday_certifications/{id}":{"get":{"summary":"Get workday_certification","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.WorkdayCertification"}}}},"404":{"description":"Not found"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/workday_loa":{"get":{"summary":"List workday_loa","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/manager-experience.WorkdayLoa"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["manager-experience"]},"post":{"summary":"Create workday_loa","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.WorkdayLoa"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.WorkdayLoa"}}}}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/workday_loa/{id}":{"get":{"summary":"Get workday_loa","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.WorkdayLoa"}}}},"404":{"description":"Not found"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/workday_time_off":{"get":{"summary":"List workday_time_off","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/manager-experience.WorkdayTimeOff"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["manager-experience"]},"post":{"summary":"Create workday_time_off","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.WorkdayTimeOff"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.WorkdayTimeOff"}}}}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/workday_time_off/{id}":{"get":{"summary":"Get workday_time_off","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.WorkdayTimeOff"}}}},"404":{"description":"Not found"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/sfsf_certifications":{"get":{"summary":"List sfsf_certifications","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/manager-experience.SfsfCertification"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["manager-experience"]},"post":{"summary":"Create sfsf_certification","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.SfsfCertification"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.SfsfCertification"}}}}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/sfsf_certifications/{id}":{"get":{"summary":"Get sfsf_certification","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.SfsfCertification"}}}},"404":{"description":"Not found"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/hr_profiles":{"get":{"summary":"List hr_profiles","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/manager-experience.HrProfileOverride"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["manager-experience"]},"post":{"summary":"Create hr_profile_override","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.HrProfileOverride"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.HrProfileOverride"}}}}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/hr_profiles/{id}":{"get":{"summary":"Get hr_profile_override","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.HrProfileOverride"}}}},"404":{"description":"Not found"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/journeys":{"get":{"summary":"List journeys","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/manager-experience.Journey"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["manager-experience"]},"post":{"summary":"Create journey","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.Journey"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.Journey"}}}}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/journeys/{id}":{"get":{"summary":"Get journey","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.Journey"}}}},"404":{"description":"Not found"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/discussion_records":{"get":{"summary":"List discussion_records","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/manager-experience.DiscussionRecord"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["manager-experience"]},"post":{"summary":"Create discussion_record","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.DiscussionRecord"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.DiscussionRecord"}}}}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/discussion_records/{id}":{"get":{"summary":"Get discussion_record","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.DiscussionRecord"}}}},"404":{"description":"Not found"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/talking_points":{"get":{"summary":"List talking_points","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/manager-experience.TalkingPoint"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["manager-experience"]},"post":{"summary":"Create talking_point","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.TalkingPoint"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.TalkingPoint"}}}}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/talking_points/{id}":{"get":{"summary":"Get talking_point","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.TalkingPoint"}}}},"404":{"description":"Not found"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/microsoft_ooo":{"get":{"summary":"List microsoft_ooo","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/manager-experience.MicrosoftOoO"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["manager-experience"]},"post":{"summary":"Create microsoft_ooo","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.MicrosoftOoO"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.MicrosoftOoO"}}}}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/microsoft_ooo/{id}":{"get":{"summary":"Get microsoft_ooo","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.MicrosoftOoO"}}}},"404":{"description":"Not found"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/kb_articles":{"get":{"summary":"List kb_articles","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/manager-experience.KbArticle"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["manager-experience"]},"post":{"summary":"Create kb_article","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.KbArticle"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.KbArticle"}}}}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/kb_articles/{id}":{"get":{"summary":"Get kb_article","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.KbArticle"}}}},"404":{"description":"Not found"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/employees":{"get":{"summary":"List employees","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/manager-experience.Employee"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["manager-experience"]},"post":{"summary":"Create employee","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.Employee"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.Employee"}}}}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/employees/{id}":{"get":{"summary":"Get employee","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.Employee"}}}},"404":{"description":"Not found"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/holidays":{"get":{"summary":"List holidays","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/manager-experience.Holiday"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["manager-experience"]},"post":{"summary":"Create holiday","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.Holiday"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.Holiday"}}}}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/holidays/{id}":{"get":{"summary":"Get holiday","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/manager-experience.Holiday"}}}},"404":{"description":"Not found"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/v1.0/me/chats":{"get":{"x-odata":{"enabled":false,"reason":"Vendor-parity fixed contract (mirrors the real MS Graph / Jira Cloud / ServiceNow Table API / Workday WQL / SFSF OData v2 wire format) — not Purple's own OData dialect. See platform/purple-suite/README.md's MS-Graph-list exception."},"summary":"MS Graph — list the caller's Teams chats","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"}],"responses":{"200":{"description":"{value:[...]}"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/v1.0/chats":{"post":{"x-odata":{"enabled":false,"reason":"Vendor-parity fixed contract (mirrors the real MS Graph / Jira Cloud / ServiceNow Table API / Workday WQL / SFSF OData v2 wire format) — not Purple's own OData dialect. See platform/purple-suite/README.md's MS-Graph-list exception."},"summary":"MS Graph — create or return the existing 1:1 Teams chat (idempotent)","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"chatType":{"type":"string","enum":["oneOnOne","group"]},"members":{"type":"array","items":{"type":"object"}}}}}}},"responses":{"200":{"description":"Existing chat returned"},"201":{"description":"New chat created"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/v1.0/chats/{chatId}/messages":{"get":{"x-odata":{"enabled":false,"reason":"Vendor-parity fixed contract (mirrors the real MS Graph / Jira Cloud / ServiceNow Table API / Workday WQL / SFSF OData v2 wire format) — not Purple's own OData dialect. See platform/purple-suite/README.md's MS-Graph-list exception."},"summary":"MS Graph — list messages in a Teams chat","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"path","name":"chatId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"{value:[...]}"}},"tags":["manager-experience"]},"post":{"x-odata":{"enabled":false,"reason":"Vendor-parity fixed contract (mirrors the real MS Graph / Jira Cloud / ServiceNow Table API / Workday WQL / SFSF OData v2 wire format) — not Purple's own OData dialect. See platform/purple-suite/README.md's MS-Graph-list exception."},"summary":"MS Graph — post a message to a Teams chat (stores in chat_messages)","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"path","name":"chatId","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"body":{"type":"object","properties":{"content":{"type":"string"},"contentType":{"type":"string","enum":["text","html"]}}}}}}}},"responses":{"201":{"description":"Message posted; retrievable via GET /v1.0/chats/{chatId}/messages"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/v1.0/me/joinedTeams":{"get":{"x-odata":{"enabled":false,"reason":"Vendor-parity fixed contract (mirrors the real MS Graph / Jira Cloud / ServiceNow Table API / Workday WQL / SFSF OData v2 wire format) — not Purple's own OData dialect. See platform/purple-suite/README.md's MS-Graph-list exception."},"summary":"MS Graph — list the caller's joined Teams","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"}],"responses":{"200":{"description":"{value:[...]}"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/v1.0/teams/{teamId}/channels":{"get":{"x-odata":{"enabled":false,"reason":"Vendor-parity fixed contract (mirrors the real MS Graph / Jira Cloud / ServiceNow Table API / Workday WQL / SFSF OData v2 wire format) — not Purple's own OData dialect. See platform/purple-suite/README.md's MS-Graph-list exception."},"summary":"MS Graph — list a team's channels","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"path","name":"teamId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"{value:[...]}"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/v1.0/teams/{teamId}/channels/{channelId}/messages":{"get":{"x-odata":{"enabled":false,"reason":"Vendor-parity fixed contract (mirrors the real MS Graph / Jira Cloud / ServiceNow Table API / Workday WQL / SFSF OData v2 wire format) — not Purple's own OData dialect. See platform/purple-suite/README.md's MS-Graph-list exception."},"summary":"MS Graph — list messages in a Teams channel","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"path","name":"teamId","required":true,"schema":{"type":"string"}},{"in":"path","name":"channelId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"{value:[...]}"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/v1.0/me/messages":{"get":{"x-odata":{"enabled":false,"reason":"Vendor-parity fixed contract (mirrors the real MS Graph / Jira Cloud / ServiceNow Table API / Workday WQL / SFSF OData v2 wire format) — not Purple's own OData dialect. See platform/purple-suite/README.md's MS-Graph-list exception."},"summary":"MS Graph — list the caller's Outlook mail","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"query","name":"$top","schema":{"type":"integer"}}],"responses":{"200":{"description":"{value:[...]}"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/v1.0/users/{email}/sendMail":{"post":{"x-odata":{"enabled":false,"reason":"Vendor-parity fixed contract (mirrors the real MS Graph / Jira Cloud / ServiceNow Table API / Workday WQL / SFSF OData v2 wire format) — not Purple's own OData dialect. See platform/purple-suite/README.md's MS-Graph-list exception."},"summary":"MS Graph — send mail from a user's mailbox (stores sent item in mail_messages)","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"path","name":"email","required":true,"schema":{"type":"string","format":"email"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["message"],"properties":{"message":{"type":"object","required":["subject","body","toRecipients"],"properties":{"subject":{"type":"string"},"body":{"type":"object","properties":{"contentType":{"type":"string","enum":["Text","HTML"]},"content":{"type":"string"}}},"toRecipients":{"type":"array","items":{"type":"object","properties":{"emailAddress":{"type":"object","properties":{"address":{"type":"string","format":"email"}}}}}}}},"saveToSentItems":{"type":"string","enum":["true","false"]}}}}}},"responses":{"202":{"description":"Accepted — sent item stored in mail_messages; retrievable via GET /v1.0/me/messages"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/v1.0/users/{identifier}":{"get":{"x-odata":{"enabled":false,"reason":"Vendor-parity fixed contract (mirrors the real MS Graph / Jira Cloud / ServiceNow Table API / Workday WQL / SFSF OData v2 wire format) — not Purple's own OData dialect. See platform/purple-suite/README.md's MS-Graph-list exception."},"summary":"MS Graph — look up a user by email or GUID (ms_graph_get_user_by_email)","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"path","name":"identifier","required":true,"schema":{"type":"string"},"example":"alice.smith0@acme.com"}],"responses":{"200":{"description":"{id, displayName, mail, userPrincipalName, givenName, surname, jobTitle, department}"},"404":{"description":"User not found"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/v1.0/users/{userId}/presence":{"get":{"x-odata":{"enabled":false,"reason":"Vendor-parity fixed contract (mirrors the real MS Graph / Jira Cloud / ServiceNow Table API / Workday WQL / SFSF OData v2 wire format) — not Purple's own OData dialect. See platform/purple-suite/README.md's MS-Graph-list exception."},"summary":"MS Graph — real-time presence + OOO settings (MS_Graph_Get_User_Presence). State from microsoft_ooo; default → Available.","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"path","name":"userId","required":true,"schema":{"type":"string"},"example":"d567639d-2a18-4c48-a7ce-442438c1732e"}],"responses":{"200":{"description":"{id, availability, activity, outOfOfficeSettings, statusMessage}"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/v1.0/users/{email}/mailboxSettings/automaticRepliesSetting":{"get":{"x-odata":{"enabled":false,"reason":"Vendor-parity fixed contract (mirrors the real MS Graph / Jira Cloud / ServiceNow Table API / Workday WQL / SFSF OData v2 wire format) — not Purple's own OData dialect. See platform/purple-suite/README.md's MS-Graph-list exception."},"summary":"MS Graph — mailbox automatic-reply settings (MS_Graph_Get_Automatic_Replies). State from microsoft_ooo; default → Disabled.","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"path","name":"email","required":true,"schema":{"type":"string","format":"email"}}],"responses":{"200":{"description":"{status, scheduledStartDateTime, scheduledEndDateTime, internalReplyMessage}"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/v1.0/users/{email}/calendarView":{"get":{"x-odata":{"enabled":false,"reason":"Vendor-parity fixed contract (mirrors the real MS Graph / Jira Cloud / ServiceNow Table API / Workday WQL / SFSF OData v2 wire format) — not Purple's own OData dialect. See platform/purple-suite/README.md's MS-Graph-list exception."},"summary":"MS Graph — that user's Out-of-Office calendar block in a date range (Get_Employee_OOO_Status, Get_Reportees_OOO_Status). Synthesized entirely from microsoft_ooo (is_ooo + calendar_start/calendar_end, falling back to scheduled_start/scheduled_end + internal_reply_message) — no dependency on the Calendar app.","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"path","name":"email","required":true,"schema":{"type":"string","format":"email"}},{"in":"query","name":"startDateTime","required":true,"schema":{"type":"string","format":"date-time"}},{"in":"query","name":"endDateTime","required":true,"schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"{value:[...]}"},"400":{"description":"startDateTime/endDateTime missing"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/rest/api/3/search/jql":{"get":{"x-odata":{"enabled":false,"reason":"Vendor-parity fixed contract (mirrors the real MS Graph / Jira Cloud / ServiceNow Table API / Workday WQL / SFSF OData v2 wire format) — not Purple's own OData dialect. See platform/purple-suite/README.md's MS-Graph-list exception."},"summary":"Jira Cloud — search issues by JQL","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"query","name":"jql","required":true,"schema":{"type":"string"},"example":"sprint ~ 'Sprint 24' AND project = 'ENG'"}],"responses":{"200":{"description":"{issues:[...], nextPageToken, isLast}"},"400":{"description":"Unsupported JQL clause for this mock"}},"tags":["manager-experience"]},"post":{"x-odata":{"enabled":false,"reason":"Vendor-parity fixed contract (mirrors the real MS Graph / Jira Cloud / ServiceNow Table API / Workday WQL / SFSF OData v2 wire format) — not Purple's own OData dialect. See platform/purple-suite/README.md's MS-Graph-list exception."},"summary":"Jira Cloud — search issues by JQL (POST body)","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"jql":{"type":"string"}}}}}},"responses":{"200":{"description":"{issues:[...], nextPageToken, isLast}"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/api/now/table/sn_hr_core_profile":{"get":{"x-odata":{"enabled":false,"reason":"Vendor-parity fixed contract (mirrors the real MS Graph / Jira Cloud / ServiceNow Table API / Workday WQL / SFSF OData v2 wire format) — not Purple's own OData dialect. See platform/purple-suite/README.md's MS-Graph-list exception."},"summary":"ServiceNow Table API — HR profile lookup","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"query","name":"sysparm_query","required":true,"schema":{"type":"string"},"example":"user.emailIN{{{user_email}}}"}],"responses":{"200":{"description":"{result:[...]}"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/api/now/table/sys_user":{"get":{"x-odata":{"enabled":false,"reason":"Vendor-parity fixed contract (mirrors the real MS Graph / Jira Cloud / ServiceNow Table API / Workday WQL / SFSF OData v2 wire format) — not Purple's own OData dialect. See platform/purple-suite/README.md's MS-Graph-list exception."},"summary":"ServiceNow Table API — sys_user lookup by email or manager.email","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"query","name":"sysparm_query","required":true,"schema":{"type":"string"},"example":"manager.email={{{manager_email}}}^active=true"}],"responses":{"200":{"description":"{result:[...]}"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/api/now/table/rm_story":{"get":{"x-odata":{"enabled":false,"reason":"Vendor-parity fixed contract (mirrors the real MS Graph / Jira Cloud / ServiceNow Table API / Workday WQL / SFSF OData v2 wire format) — not Purple's own OData dialect. See platform/purple-suite/README.md's MS-Graph-list exception."},"summary":"ServiceNow Table API — rm_story lookup for a manager's direct reports (Snow_Get_Reportee_Stories, Servicenow_get_work_items_of_my_reportees)","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"query","name":"sysparm_query","required":true,"schema":{"type":"string"},"example":"assigned_to.manager.email=alice.smith0@acme.com^state=2"},{"in":"query","name":"sysparm_fields","schema":{"type":"string"}},{"in":"query","name":"sysparm_display_value","schema":{"type":"string"}},{"in":"query","name":"sysparm_limit","schema":{"type":"integer"}}],"responses":{"200":{"description":"{result:[{number, short_description, description, acceptance_criteria, assigned_to:{display_value,link}, assignment_group:{display_value,link}, project:{display_value,link}, state, blocked, blocked_reason, priority, story_points, end_date, epic, sprint, \"sprint.start_date\", \"sprint.end_date\", \"sprint.committed_points\", \"sprint.story_points\", \"sprint.actual_points\", sys_id, sys_updated_on}]}"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/api/now/table/rm_defect":{"get":{"x-odata":{"enabled":false,"reason":"Vendor-parity fixed contract (mirrors the real MS Graph / Jira Cloud / ServiceNow Table API / Workday WQL / SFSF OData v2 wire format) — not Purple's own OData dialect. See platform/purple-suite/README.md's MS-Graph-list exception."},"summary":"ServiceNow Table API — rm_defect lookup for a manager's direct reports (Snow_Get_Reportee_Defects, Servicenow_get_work_items_of_my_reportees)","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"query","name":"sysparm_query","required":true,"schema":{"type":"string"},"example":"assigned_to.manager.email=alice.smith0@acme.com^end_date<2026-08-11^stateNOT IN3,4,7"},{"in":"query","name":"sysparm_fields","schema":{"type":"string"}},{"in":"query","name":"sysparm_display_value","schema":{"type":"string"}},{"in":"query","name":"sysparm_limit","schema":{"type":"integer"}}],"responses":{"200":{"description":"{result:[{number, short_description, description, assigned_to:{display_value,link}, assignment_group:{display_value,link}, state, priority, end_date, sprint, \"sprint.start_date\", \"sprint.end_date\", \"sprint.committed_points\", sys_id, sys_updated_on}]}"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/api/now/stats/resource_aggregate_weekly":{"get":{"x-odata":{"enabled":false,"reason":"Vendor-parity fixed contract (mirrors the real MS Graph / Jira Cloud / ServiceNow Table API / Workday WQL / SFSF OData v2 wire format) — not Purple's own OData dialect. See platform/purple-suite/README.md's MS-Graph-list exception."},"summary":"ServiceNow Stats API — per-person, per-project resource allocation aggregates for a manager's direct reports (Servicenow_Get_Team_Project_Allocations, plugin Team_Project_Allocations)","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"query","name":"sysparm_query","required":true,"schema":{"type":"string"},"example":"user.manager.email=alice.smith0@acme.com^taskISNOTEMPTY^task.active=true"},{"in":"query","name":"sysparm_group_by","schema":{"type":"string"}},{"in":"query","name":"sysparm_sum_fields","schema":{"type":"string"}},{"in":"query","name":"sysparm_avg_fields","schema":{"type":"string"}},{"in":"query","name":"sysparm_min_fields","schema":{"type":"string"}},{"in":"query","name":"sysparm_max_fields","schema":{"type":"string"}},{"in":"query","name":"sysparm_display_value","schema":{"type":"string"}}],"responses":{"200":{"description":"{result:[{stats:{max:{week_starts_on}, avg:{fte, utilization}, min:{week_starts_on}, sum:{hours, man_days}}, groupby_fields:[{field:'category',value}, {field:'task',value}, {field:'task.percent_complete', value?}, {field:'task.short_description',value}, {field:'task.state',value}, {field:'task.sys_class_name',value}, {field:'user',value}]}]} — this is ServiceNow's aggregate Stats API (/api/now/stats/{table}), NOT the Table API rm_story/rm_defect use — a different real envelope, grouped rows with sum/avg/min/max, not raw records. 'category' and 'utilization' are returned by the real endpoint even though sysparm_group_by/sysparm_avg_fields don't request them. A dot-walked groupby field (e.g. task.percent_complete) omits its 'value' key entirely when null, rather than returning value:'' — only the non-dot-walked 'task' field gets an explicit empty string."}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/api/snc/manager_hub/reportees/{email}":{"get":{"x-odata":{"enabled":false,"reason":"Vendor-parity fixed contract (mirrors the real MS Graph / Jira Cloud / ServiceNow Table API / Workday WQL / SFSF OData v2 wire format) — not Purple's own OData dialect. See platform/purple-suite/README.md's MS-Graph-list exception."},"summary":"ServiceNow Custom API — a manager's direct reports","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"path","name":"email","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"{result:[...]}"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/api/sn_mgr_exp/manager_view_as/users":{"get":{"x-odata":{"enabled":false,"reason":"Vendor-parity fixed contract (mirrors the real MS Graph / Jira Cloud / ServiceNow Table API / Workday WQL / SFSF OData v2 wire format) — not Purple's own OData dialect. See platform/purple-suite/README.md's MS-Graph-list exception."},"summary":"ServiceNow Custom API — resolve a reportee by name (direct + skip-level + matrix) (Resolve_Reportee)","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"query","name":"mwUserEmail","required":true,"schema":{"type":"string"},"example":"alice.smith0@acme.com"},{"in":"query","name":"name","required":true,"schema":{"type":"string"},"example":"Bob"}],"responses":{"200":{"description":"{result:{success, count, users:[{user_name, display_name, title, department, email, sys_id, location}]}}"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/api/sn_mgr_exp/manager_view_as/search":{"post":{"x-odata":{"enabled":false,"reason":"Vendor-parity fixed contract (mirrors the real MS Graph / Jira Cloud / ServiceNow Table API / Workday WQL / SFSF OData v2 wire format) — not Purple's own OData dialect. See platform/purple-suite/README.md's MS-Graph-list exception."},"summary":"ServiceNow Custom API — search knowledge articles as a reportee (Manager_View_As_Reportee)","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"mwUserEmail":{"type":"string"},"view_as_user_id":{"type":"string"},"query":{"type":"string"}}},"example":{"mwUserEmail":"alice.smith0@acme.com","view_as_user_id":"3f9c2a1e8b7d4650a1c2e3f4a5b6c7d8","query":"remote work"}}}},"responses":{"200":{"description":"{result:{total_count, view_as_user_id, view_as_user_name, articles_markdown, articles:[...]}}"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/api/sn_mgr_exp/mw_holiday_api/holiday_list":{"post":{"x-odata":{"enabled":false,"reason":"Vendor-parity fixed contract (mirrors the real MS Graph / Jira Cloud / ServiceNow Table API / Workday WQL / SFSF OData v2 wire format) — not Purple's own OData dialect. See platform/purple-suite/README.md's MS-Graph-list exception."},"summary":"ServiceNow Custom API — a reportee's or team's upcoming public holidays (View_Holiday_List)","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"moveworks_user_email":{"type":"string"},"employee_sys_id":{"type":"string"},"lookahead_days":{"type":"string"},"filter_country":{"type":"string"},"team_view":{"type":"string"}}},"example":{"moveworks_user_email":"alice.smith0@acme.com","employee_sys_id":"9a3f2b1c4d5e6f708192a3b4c5d6e7f8","lookahead_days":"365","filter_country":"US","team_view":"false"}}}},"responses":{"200":{"description":"{result:{success, team_size, count, searched_as, error, error_code, results:{holiday:[...]}}}"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/ccx/api/wql/v1/{tenant}/data":{"post":{"x-odata":{"enabled":false,"reason":"Vendor-parity fixed contract (mirrors the real MS Graph / Jira Cloud / ServiceNow Table API / Workday WQL / SFSF OData v2 wire format) — not Purple's own OData dialect. See platform/purple-suite/README.md's MS-Graph-list exception."},"summary":"Workday WQL — allWorkers / allActiveAndTerminatedWorkers / learningEnrollments","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"path","name":"tenant","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"query":{"type":"string"}},"example":{"query":"SELECT workdayID, fullName, email_PrimaryWork, employeeID, supervisoryOrganization FROM allWorkers WHERE email_PrimaryWork = 'alice.smith0@acme.com'"}}}}},"responses":{"200":{"description":"{data:[...], total}"},"400":{"description":"Unsupported WQL query for this mock"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/api/v1/{tenant}/workers/{workerId}/directReports":{"get":{"x-odata":{"enabled":false,"reason":"Vendor-parity fixed contract (mirrors the real MS Graph / Jira Cloud / ServiceNow Table API / Workday WQL / SFSF OData v2 wire format) — not Purple's own OData dialect. See platform/purple-suite/README.md's MS-Graph-list exception."},"summary":"Workday REST — a worker's direct reports","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"path","name":"tenant","required":true,"schema":{"type":"string"}},{"in":"path","name":"workerId","required":true,"schema":{"type":"string"},"example":"WD-0001"}],"responses":{"200":{"description":"{total, directReports:[...]}"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/api/absenceManagement/v5/{tenant}/workers/{workerId}/leavesOfAbsence":{"get":{"x-odata":{"enabled":false,"reason":"Vendor-parity fixed contract (mirrors the real MS Graph / Jira Cloud / ServiceNow Table API / Workday WQL / SFSF OData v2 wire format) — not Purple's own OData dialect. See platform/purple-suite/README.md's MS-Graph-list exception."},"summary":"Workday Absence Management REST — a worker's in-progress (active or upcoming) leaves of absence (Workday_Get_Worker_LOA)","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"path","name":"tenant","required":true,"schema":{"type":"string"},"example":"bcone_gms1"},{"in":"path","name":"workerId","required":true,"schema":{"type":"string"},"example":"WD-0004"},{"in":"query","name":"status","schema":{"type":"string"},"example":"e2d08afc53614c37b32b31270bb8bee3"},{"in":"query","name":"fromDate","schema":{"type":"string","format":"date"},"example":"2026-01-01"},{"in":"query","name":"toDate","schema":{"type":"string","format":"date"},"example":"2026-06-06"},{"in":"query","name":"limit","schema":{"type":"integer"}},{"in":"query","name":"offset","schema":{"type":"integer"}}],"responses":{"200":{"description":"{total, data:[{id, worker:{id,descriptor}, leaveType:{id,descriptor}, status:{id,descriptor}, firstDayOfLeave, lastDayOfWork, estimatedLastDayOfLeave, leaveImpactFields, additionalFields, ...}]}"},"404":{"description":"No worker found for id"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/ccx/api/absenceManagement/v1/{tenant}/workers/{workerId}/timeOffDetails":{"get":{"x-odata":{"enabled":false,"reason":"Vendor-parity fixed contract (mirrors the real MS Graph / Jira Cloud / ServiceNow Table API / Workday WQL / SFSF OData v2 wire format) — not Purple's own OData dialect. See platform/purple-suite/README.md's MS-Graph-list exception."},"summary":"Workday Absence Management REST — a worker's individual time-off (PTO) entries (Workday_Get_Worker_PTO)","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"path","name":"tenant","required":true,"schema":{"type":"string"},"example":"bcone_gms1"},{"in":"path","name":"workerId","required":true,"schema":{"type":"string"},"example":"WD-0001"},{"in":"query","name":"fromDate","schema":{"type":"string","format":"date"},"example":"2026-01-01"},{"in":"query","name":"toDate","schema":{"type":"string","format":"date"},"example":"2026-01-08"},{"in":"query","name":"limit","schema":{"type":"integer"},"example":50}],"responses":{"200":{"description":"{total, data:[{id, date, quantity, unitOfTime, timeOffType:{id,descriptor}, status:{id,descriptor}, worker:{id,descriptor}}]}"},"404":{"description":"No worker found for id"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/odata/v2/User":{"get":{"x-odata":{"enabled":false,"reason":"Vendor-parity fixed contract (mirrors the real MS Graph / Jira Cloud / ServiceNow Table API / Workday WQL / SFSF OData v2 wire format) — not Purple's own OData dialect. See platform/purple-suite/README.md's MS-Graph-list exception."},"summary":"SAP SuccessFactors OData v2 — User entity","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"query","name":"$filter","schema":{"type":"string"},"example":"manager eq 'alice.smith0@acme.com'"}],"responses":{"200":{"description":"{d:{results:[...]}}"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/odata/v2/Background_Certificates":{"get":{"x-odata":{"enabled":false,"reason":"Vendor-parity fixed contract (mirrors the real MS Graph / Jira Cloud / ServiceNow Table API / Workday WQL / SFSF OData v2 wire format) — not Purple's own OData dialect. See platform/purple-suite/README.md's MS-Graph-list exception."},"summary":"SAP SuccessFactors OData v2 — Background_Certificates entity","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"query","name":"$filter","schema":{"type":"string"},"example":"userId eq 'SF0001'"}],"responses":{"200":{"description":"{d:{results:[...]}}"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/api/now/table/sn_egd_act_discussion":{"get":{"x-odata":{"enabled":false,"reason":"Vendor-parity fixed contract (mirrors the real MS Graph / Jira Cloud / ServiceNow Table API / Workday WQL / SFSF OData v2 wire format) — not Purple's own OData dialect. See platform/purple-suite/README.md's MS-Graph-list exception."},"summary":"ServiceNow Table API — find the active 1:1 discussion record for an employee (Fetch_sys_id_of_the_Discussion_record)","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"query","name":"sysparm_query","required":true,"schema":{"type":"string"},"example":"discussion_series.employee.user.sys_id={{{employee_sysid}}}^active=true"},{"in":"query","name":"sysparm_fields","schema":{"type":"string"},"example":"sys_id"},{"in":"query","name":"sysparm_limit","schema":{"type":"string"},"example":"1"},{"in":"query","name":"sysparm_display_value","schema":{"type":"string"},"example":"true"}],"responses":{"200":{"description":"{result:[...]}"},"400":{"description":"sysparm_query missing the required employee sys_id clause"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/api/now/table/sn_egd_act_discussion/{sys_id}":{"patch":{"x-odata":{"enabled":false,"reason":"Vendor-parity fixed contract (mirrors the real MS Graph / Jira Cloud / ServiceNow Table API / Workday WQL / SFSF OData v2 wire format) — not Purple's own OData dialect. See platform/purple-suite/README.md's MS-Graph-list exception."},"summary":"ServiceNow Table API — save a generated brief onto a discussion record's u_agenda field (Servicenow_Save_Brief_on_Conversation_Record)","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"path","name":"sys_id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"u_agenda":{"type":"string"}}},"example":{"u_agenda":"Discussed Q3 goals and career growth plan."}}}},"responses":{"200":{"description":"{result:{...full DiscussionRecord wire shape}}"},"404":{"description":"No record with that sys_id"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/api/now/table/sn_employee_profile":{"get":{"x-odata":{"enabled":false,"reason":"Vendor-parity fixed contract (mirrors the real MS Graph / Jira Cloud / ServiceNow Table API / Workday WQL / SFSF OData v2 wire format) — not Purple's own OData dialect. See platform/purple-suite/README.md's MS-Graph-list exception."},"summary":"ServiceNow Table API — resolve a user's Employee Profile sys_id (get_employee_profile_details)","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"query","name":"sysparm_query","required":true,"schema":{"type":"string"},"example":"user={{{user_id}}}"},{"in":"query","name":"sysparm_limit","schema":{"type":"string"},"example":"1"},{"in":"query","name":"sysparm_fields","schema":{"type":"string"},"example":"number, sys_id, hr_profile"}],"responses":{"200":{"description":"{result:[{number, sys_id, hr_profile}]}"},"400":{"description":"sysparm_query missing the required user=<sys_id> clause"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/api/now/table/sn_egd_act_talking_point":{"get":{"x-odata":{"enabled":false,"reason":"Vendor-parity fixed contract (mirrors the real MS Graph / Jira Cloud / ServiceNow Table API / Workday WQL / SFSF OData v2 wire format) — not Purple's own OData dialect. See platform/purple-suite/README.md's MS-Graph-list exception."},"summary":"ServiceNow Table API — list a manager's open talking points for a reportee (list_talking_points)","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"query","name":"sysparm_query","required":true,"schema":{"type":"string"},"example":"added_by={{{added_by}}}^u_added_for={{{u_added_for}}}^active=true"},{"in":"query","name":"sysparm_fields","schema":{"type":"string"},"example":"sys_id,title,sys_created_on"},{"in":"query","name":"sysparm_limit","schema":{"type":"string"},"example":"100"}],"responses":{"200":{"description":"{result:[...]}"},"400":{"description":"sysparm_query missing added_by=<x>^u_added_for=<y>"}},"tags":["manager-experience"]},"post":{"x-odata":{"enabled":false,"reason":"Vendor-parity fixed contract (mirrors the real MS Graph / Jira Cloud / ServiceNow Table API / Workday WQL / SFSF OData v2 wire format) — not Purple's own OData dialect. See platform/purple-suite/README.md's MS-Graph-list exception."},"summary":"ServiceNow Table API — create a new talking point (create_talking_point)","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["title","added_by","u_added_for"],"properties":{"title":{"type":"string"},"added_by":{"type":"string"},"u_added_for":{"type":"string"},"active":{"type":"string"}}},"example":{"title":"Discuss sprint velocity dip in Sprint 24","added_by":"976865a397ff095020dbbdc3f153af78","u_added_for":"f714756f97ff095020dbbdc3f153afb4","active":"true"}}}},"responses":{"201":{"description":"{result:{...full TalkingPoint wire shape}}"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/api/now/table/sn_egd_act_talking_point/{sys_id}":{"patch":{"x-odata":{"enabled":false,"reason":"Vendor-parity fixed contract (mirrors the real MS Graph / Jira Cloud / ServiceNow Table API / Workday WQL / SFSF OData v2 wire format) — not Purple's own OData dialect. See platform/purple-suite/README.md's MS-Graph-list exception."},"summary":"ServiceNow Table API — edit a talking point's title, or soft-delete it via active=false (update_talking_point / delete_talking_point share this route)","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"path","name":"sys_id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string"},"active":{"type":"string"}}},"example":{"title":"Discuss updated sprint plan"}}}},"responses":{"200":{"description":"{result:{...full TalkingPoint wire shape}}"},"404":{"description":"No record with that sys_id"}},"tags":["manager-experience"]}},"/purple-suite/manager-experience/api/sn_jny/unified_journey/sn_mx/users/{user_id}/journeys":{"get":{"x-odata":{"enabled":false,"reason":"Vendor-parity fixed contract (mirrors the real MS Graph / Jira Cloud / ServiceNow Table API / Workday WQL / SFSF OData v2 wire format) — not Purple's own OData dialect. See platform/purple-suite/README.md's MS-Graph-list exception."},"summary":"ServiceNow Unified Journey (sn_jny) — a user's onboarding/career journeys (Fetch_Journey_Details)","parameters":[{"$ref":"#/components/parameters/manager-experience.InstanceId"},{"in":"path","name":"user_id","required":true,"schema":{"type":"string"},"description":"ServiceNow sys_id of the employee."}],"responses":{"200":{"description":"{result:{status, journeys:[...]}}"}},"tags":["manager-experience"]}},"/purple-suite/pages/pages":{"get":{"summary":"List wiki pages in the Purple Pages workspace","parameters":[{"$ref":"#/components/parameters/pages.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/pages.Page"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"description":"Purple Pages is the Notion-style knowledge workspace: a Page is a rich-text document (title, emoji, block content, sharing) that can nest under a parent_id. Use this to find documentation, meeting notes or specs; structured tabular data lives in pages__list__databases / pages__list__db_records instead. Supports server-side OData querying — $filter, $search, $orderby, $select, $top, $skip, $count (MCP clients see these as _filter, _search, _orderby, _select, _top, _skip, _count because $ is not a legal tool-input key). Example: $filter=parent_id eq null for top-level pages, or $search=onboarding.","tags":["pages"]},"post":{"summary":"Create a wiki page","parameters":[{"$ref":"#/components/parameters/pages.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/pages.Page"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pages.Page"}}}}},"description":"Creates a new document in the Purple Pages workspace with a title and optional emoji, block content, parent_id (to nest it under another page) and sharing settings. Create a Database instead if the user wants rows and columns rather than prose.","tags":["pages"]}},"/purple-suite/pages/pages/{id}":{"get":{"summary":"Get one wiki page with its content","parameters":[{"$ref":"#/components/parameters/pages.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pages.Page"}}}},"404":{"description":"Not found"}},"description":"Fetches a single Page by id, including its full block content array — this is how you read what a page actually says. List operations return the same shape, so use this when you already have the page id.","tags":["pages"]},"patch":{"summary":"Edit a wiki page's title, content or sharing","description":"Partially updates a Page: title, emoji, block content, lastEditedAt, shared/sharedWith and permissions. This is the tool for editing what a page SAYS. To change where a page sits in the hierarchy, use pages__move__pages rather than patching parent_id.","parameters":[{"$ref":"#/components/parameters/pages.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pages.Page"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pages.Page"}}}},"404":{"description":"Not found"}},"tags":["pages"]},"delete":{"summary":"Delete a wiki page","parameters":[{"$ref":"#/components/parameters/pages.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"description":"DESTRUCTIVE and irreversible: permanently deletes the page and its content (there is no trash or undo). Child pages that reference it as parent_id are orphaned and its comments are left dangling. Confirm with the user before calling.","tags":["pages"]}},"/purple-suite/pages/databases":{"get":{"summary":"List structured databases in Purple Pages","parameters":[{"$ref":"#/components/parameters/pages.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/pages.Database"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"description":"A Database is a Notion-style structured table in the workspace (name, viewType table/board/gallery/list/calendar, and a schema listing its column names) — for example a Project Tracker or Vendor Directory. This returns the table definitions only; the rows inside them come from pages__list__db_records. Supports server-side OData querying — $filter, $search, $orderby, $select, $top, $skip, $count (MCP clients see these as _filter, _search, _orderby, _select, _top, _skip, _count because $ is not a legal tool-input key).","tags":["pages"]},"post":{"summary":"Create a structured database","parameters":[{"$ref":"#/components/parameters/pages.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/pages.Database"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pages.Database"}}}}},"description":"Creates a new Notion-style table in the workspace: name, viewType and a schema array of column names. Creates the container only — add rows afterwards with pages__create__db_records.","tags":["pages"]}},"/purple-suite/pages/databases/{id}":{"get":{"summary":"Get one database definition","parameters":[{"$ref":"#/components/parameters/pages.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pages.Database"}}}},"404":{"description":"Not found"}},"description":"Fetches a single Database by id with its schema (column names), viewType and sharing. Call this before writing records so the field names you send match the table's schema.","tags":["pages"]},"patch":{"summary":"Update a database's name, view or schema","description":"Partially updates the table definition (name, viewType, schema columns, sharing). Editing the schema does not rewrite existing rows — change row values with pages__update__db_records.","parameters":[{"$ref":"#/components/parameters/pages.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pages.Database"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pages.Database"}}}},"404":{"description":"Not found"}},"tags":["pages"]},"delete":{"summary":"Delete a structured database","parameters":[{"$ref":"#/components/parameters/pages.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"description":"DESTRUCTIVE and irreversible: removes the table definition. Its records are not deleted with it and become orphaned rows pointing at a missing databaseId, so confirm with the user first.","tags":["pages"]}},"/purple-suite/pages/db-records":{"get":{"summary":"List rows inside Purple Pages databases","parameters":[{"$ref":"#/components/parameters/pages.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."},{"in":"query","name":"databaseId","required":false,"schema":{"type":"string"},"description":"Filter records by parent database ID. When omitted all records across all databases are returned.","example":"a1b2c3d4-e5f6-7890-abcd-ef1234567890"}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/pages.DbRecord"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"description":"Returns the actual rows of the workspace's Notion-style tables — each DbRecord belongs to a databaseId and carries that table's user-defined fields (title, status, owner, priority, severity, dueDate, …). Almost always scope it with the databaseId query param, or you get rows from every table at once. Supports server-side OData querying — $filter, $search, $orderby, $select, $top, $skip, $count (MCP clients see these as _filter, _search, _orderby, _select, _top, _skip, _count because $ is not a legal tool-input key). Example: databaseId=<id> with $filter=status eq 'In Progress'.","tags":["pages"]},"post":{"summary":"Add a row to a Purple Pages database","parameters":[{"$ref":"#/components/parameters/pages.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/pages.DbRecord"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pages.DbRecord"}}}}},"description":"Creates a record inside an existing database — set databaseId to the parent table and supply fields matching that table's schema (check it with pages__get__databases first). Use this for tracker rows; use pages__create__pages for a prose document.","tags":["pages"]}},"/purple-suite/pages/db-records/{id}":{"get":{"summary":"Get one database row","parameters":[{"$ref":"#/components/parameters/pages.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pages.DbRecord"}}}},"404":{"description":"Not found"}},"description":"Fetches a single DbRecord by id with all of its field values. Use it after locating the row with pages__list__db_records.","tags":["pages"]},"patch":{"summary":"Update fields on a database row","description":"Partially updates one row in a Purple Pages database — this is how you change a tracker item's status, owner, priority or due date. Send only the fields you want to change; unspecified fields are preserved.","parameters":[{"$ref":"#/components/parameters/pages.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pages.DbRecord"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pages.DbRecord"}}}},"404":{"description":"Not found"}},"tags":["pages"]},"delete":{"summary":"Delete a database row","parameters":[{"$ref":"#/components/parameters/pages.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"description":"DESTRUCTIVE and irreversible: permanently removes the row from its database, with no trash or undo. If the intent is to mark work finished, patch its status field instead.","tags":["pages"]}},"/purple-suite/pages/comments":{"get":{"summary":"List comments left on wiki pages","parameters":[{"$ref":"#/components/parameters/pages.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."},{"in":"query","name":"page_id","required":false,"schema":{"type":"string"},"description":"Filter comments by the page they are attached to. When omitted all comments across all pages are returned.","example":"a1b2c3d4-e5f6-7890-abcd-ef1234567890"}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/pages.Comment"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"description":"Returns Notion-style page comments — body, author (createdBy), the page_id they hang off and an optional discussion_id thread. Scope with the page_id query param to read one page's discussion; without it you get comments across the whole workspace. Supports server-side OData querying — $filter, $search, $orderby, $select, $top, $skip, $count (MCP clients see these as _filter, _search, _orderby, _select, _top, _skip, _count because $ is not a legal tool-input key).","tags":["pages"]},"post":{"summary":"Comment on a wiki page","parameters":[{"$ref":"#/components/parameters/pages.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/pages.Comment"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pages.Comment"}}}}},"description":"Adds a comment to a page: set page_id to the target page and body to the comment text (pass discussion_id to reply within an existing thread). This annotates the page without changing it — to edit the page body itself use pages__update__pages.","tags":["pages"]}},"/purple-suite/pages/comments/{id}":{"get":{"summary":"Get one page comment","parameters":[{"$ref":"#/components/parameters/pages.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pages.Comment"}}}},"404":{"description":"Not found"}},"description":"Fetches a single comment by id with its body, author and parent page_id. Use it after finding the comment via pages__list__comments.","tags":["pages"]},"patch":{"summary":"Edit a page comment","description":"Partially updates an existing comment, normally its body text. Does not touch the page content the comment is attached to.","parameters":[{"$ref":"#/components/parameters/pages.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pages.Comment"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pages.Comment"}}}},"404":{"description":"Not found"}},"tags":["pages"]},"delete":{"summary":"Delete a page comment","parameters":[{"$ref":"#/components/parameters/pages.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"description":"DESTRUCTIVE and irreversible: permanently removes the comment from the page discussion. The page itself is unaffected.","tags":["pages"]}},"/purple-suite/pages/pages/{id}/move":{"post":{"summary":"Move a page under a new parent","description":"Changes only WHERE a page sits in the Purple Pages hierarchy — reparents it by setting parent_id (pass null to move it to the top level). Use this for 'move X under Y' or 'move X to the top level'; use pages__update__pages to change the page's title, content or sharing. The page's content and comments are unaffected.","parameters":[{"$ref":"#/components/parameters/pages.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"},"example":"a1b2c3d4-e5f6-7890-abcd-ef1234567890"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"parent_id":{"type":"string","nullable":true,"example":"notion_page_agent_studio_corner","description":"ID of the new parent page, or null to move to the top level"}},"required":["parent_id"]}}}},"responses":{"200":{"description":"Updated page after move","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pages.Page"}}}},"400":{"description":"parent_id is required"},"404":{"description":"Page not found"}},"tags":["pages"]}},"/purple-suite/pages/pages/{id}/duplicate":{"post":{"summary":"Duplicate a page","description":"Creates a brand-new page that copies the source page's content, emoji and other fields, titled \"<title> (Copy)\", and returns it with its new id. Handy for using an existing page as a template; the original is left untouched. Comments on the source page are not copied.","parameters":[{"$ref":"#/components/parameters/pages.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"},"example":"a1b2c3d4-e5f6-7890-abcd-ef1234567890"}],"responses":{"201":{"description":"The newly created duplicate page","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pages.Page"}}}},"404":{"description":"Page not found"}},"tags":["pages"]}},"/purple-suite/pm/projects":{"get":{"summary":"List projects","description":"Purple Planner projects are the delivery workstreams that hold tasks, sprints and milestones, each with an owner, status, dates, progress and budget. This is project delivery, not a CRM account or an ERP cost centre. Filter/sort server-side with $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send these as _filter, _orderby, …) rather than listing everything and filtering locally.","parameters":[{"$ref":"#/components/parameters/pm.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/pm.Project"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["pm"]},"post":{"summary":"Create project","description":"Open a new delivery workstream in Purple Planner. Only for genuinely new projects — to change an existing one use the update-project tool, and to file a unit of work use create-task instead.","parameters":[{"$ref":"#/components/parameters/pm.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/pm.Project"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pm.Project"}}}}},"tags":["pm"]}},"/purple-suite/pm/projects/{id}":{"get":{"summary":"Get project","description":"Fetch one Purple Planner project by id (e.g. PROJ-001) with its owner, status, dates, progress, budget and the initiatives/teams it rolls up to.","parameters":[{"$ref":"#/components/parameters/pm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pm.Project"}}}},"404":{"description":"Not found"}},"tags":["pm"]},"patch":{"summary":"Update project","description":"Partially update a Purple Planner project — status, owner, dates, progress, budget, initiativeIds or teamIds. Status changes are state-machine checked: a completed project cannot be reactivated (422).","parameters":[{"$ref":"#/components/parameters/pm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pm.Project"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pm.Project"}}}},"404":{"description":"Not found"},"422":{"description":"State machine violation — e.g. cannot reactivate a completed project"}},"tags":["pm"]},"delete":{"summary":"Delete project","description":"Permanently delete a Purple Planner project. Destructive and irreversible, and its tasks/sprints keep a dangling projectId — prefer setting status to completed or cancelled with the update-project tool unless the record was created in error.","parameters":[{"$ref":"#/components/parameters/pm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["pm"]}},"/purple-suite/pm/tasks":{"get":{"summary":"List tasks","description":"Purple Planner tasks (issues) are the unit of work inside a project, carrying assignee, priority, story points, sprint, labels, dependencies and blockers. These are delivery issues, not ITSM incidents/service requests and not HR cases. Filter/sort server-side with $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send these as _filter, _orderby, …) rather than listing everything and filtering locally.","parameters":[{"$ref":"#/components/parameters/pm.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/pm.Task"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}}},"tags":["pm"]},"post":{"summary":"Create task","description":"File a new task (issue) in a Purple Planner project, optionally with assignee, priority, story points, sprint and labels. Returns 422 if the target project is already completed or cancelled.","parameters":[{"$ref":"#/components/parameters/pm.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/pm.Task"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pm.Task"}}}},"422":{"description":"State machine violation — e.g. cannot add a task to a completed or cancelled project"}},"tags":["pm"]}},"/purple-suite/pm/tasks/{id}":{"get":{"summary":"Get task","description":"Fetch one Purple Planner task (issue) by id with its status, assignee, sprintId, labels, dependencies, blockers and openSubtasks — use it to check whether a task is actually completable.","parameters":[{"$ref":"#/components/parameters/pm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pm.Task"}}}},"404":{"description":"Not found"}},"tags":["pm"]},"patch":{"summary":"Update task","description":"Partially update a Purple Planner task — retitle, reassign, reprioritise, move it to another sprint, or change status. Completion is guarded: moving to done/closed fails with 422 while openSubtasks > 0 or blockers is non-empty, and reopening a cancelled task requires reopenReason. To record that this task waits on another, use the add-dependency tool instead.","parameters":[{"$ref":"#/components/parameters/pm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pm.Task"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pm.Task"}}}},"404":{"description":"Not found"},"422":{"description":"State machine violation — e.g. task has open subtasks or active blockers preventing completion, or reopenReason missing when reopening a cancelled task"}},"tags":["pm"]},"delete":{"summary":"Delete task","description":"Permanently delete a Purple Planner task. Destructive and irreversible — to finish work normally set status to done/closed with the update-task tool; only delete issues filed by mistake.","parameters":[{"$ref":"#/components/parameters/pm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["pm"]}},"/purple-suite/pm/tasks/{id}/add-dependency":{"post":{"summary":"Add a task dependency","description":"Record that this Purple Planner task is blocked until dependency_id finishes, appending to its dependencies array. Use this rather than patching the task, because the server rejects (400) any edge that would create a circular dependency.","parameters":[{"$ref":"#/components/parameters/pm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"dependency_id":{"type":"string","example":"TASK-0002","description":"ID of the task that must complete before this task"}},"required":["dependency_id"]}}}},"responses":{"200":{"description":"Updated task with new dependency","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pm.Task"}}}},"400":{"description":"dependency_id missing or circular dependency detected"},"404":{"description":"Task or dependency task not found"}},"tags":["pm"]}},"/purple-suite/pm/sprints":{"get":{"summary":"List sprints","description":"Purple Planner sprints are the time-boxed iterations (planned | active | completed) of a project that tasks are assigned to via Task.sprintId, with velocity and completedPoints. Filter/sort server-side with $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send these as _filter, _orderby, …) rather than listing everything and filtering locally.","parameters":[{"$ref":"#/components/parameters/pm.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/pm.Sprint"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["pm"]},"post":{"summary":"Create sprint","description":"Plan a new time-boxed iteration on a Purple Planner project. New sprints are 'planned'; activate one with the start-sprint tool rather than by creating another.","parameters":[{"$ref":"#/components/parameters/pm.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/pm.Sprint"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pm.Sprint"}}}}},"tags":["pm"]}},"/purple-suite/pm/sprints/{id}":{"get":{"summary":"Get sprint","description":"Fetch one Purple Planner sprint by id with its dates, status, velocity, completedPoints and startedAt/completedAt stamps — the basis for burndown or sprint-health answers.","parameters":[{"$ref":"#/components/parameters/pm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pm.Sprint"}}}},"404":{"description":"Not found"}},"tags":["pm"]},"patch":{"summary":"Update sprint","description":"Partially update sprint metadata such as name, dates or velocity. For lifecycle changes use start-sprint / complete-sprint instead — those enforce the rules and move the tasks.","parameters":[{"$ref":"#/components/parameters/pm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pm.Sprint"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pm.Sprint"}}}},"404":{"description":"Not found"}},"tags":["pm"]},"delete":{"summary":"Delete sprint","description":"Permanently delete a Purple Planner sprint. Destructive and irreversible, and tasks keep a dangling sprintId — to close out an iteration properly use complete-sprint, which rehomes unfinished work.","parameters":[{"$ref":"#/components/parameters/pm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["pm"]}},"/purple-suite/pm/sprints/{id}/start":{"post":{"summary":"Start a sprint","description":"Activate a planned Purple Planner sprint and stamp startedAt. Requires at least one task assigned to it (Task.sprintId), otherwise 422. Use this rather than patching status directly.","parameters":[{"$ref":"#/components/parameters/pm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Sprint started","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pm.Sprint"}}}},"404":{"description":"Not found"},"422":{"description":"Sprint has no tasks assigned and cannot be started"}},"tags":["pm"]}},"/purple-suite/pm/sprints/{id}/complete":{"post":{"summary":"Complete a sprint","description":"Close out a Purple Planner sprint and sweep its unfinished tasks (status != done) into next_sprint_id, or back to the backlog (sprintId=null, status=todo) when that is omitted. Irreversible, and it rewrites those tasks — the response reports moved_tasks and warns when open work was moved.","parameters":[{"$ref":"#/components/parameters/pm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"next_sprint_id":{"type":"string","example":"SPR-002","description":"Sprint ID to move incomplete tasks into; omit to return tasks to backlog"}}}}}},"responses":{"200":{"description":"Sprint completed","content":{"application/json":{"schema":{"type":"object","properties":{"sprint":{"$ref":"#/components/schemas/pm.Sprint"},"moved_tasks":{"type":"integer","example":3,"description":"Number of incomplete tasks moved or returned to backlog"},"warning":{"type":"string","nullable":true,"example":"Sprint completed with 3 open task(s) moved to backlog."}}}}}},"404":{"description":"Not found"}},"tags":["pm"]}},"/purple-suite/pm/milestones":{"get":{"summary":"List milestones","description":"Purple Planner milestones are dated delivery checkpoints on a project (upcoming | in_progress | completed | at_risk), coarser than tasks and independent of sprints. Use these for \"are we going to hit the date\" questions. Filter/sort server-side with $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send these as _filter, _orderby, …) rather than listing everything and filtering locally.","parameters":[{"$ref":"#/components/parameters/pm.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/pm.Milestone"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["pm"]},"post":{"summary":"Create milestone","description":"Add a dated delivery checkpoint to a Purple Planner project. Use a milestone for a commitment date (e.g. 'MVP complete'), and a task for work someone has to do.","parameters":[{"$ref":"#/components/parameters/pm.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/pm.Milestone"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pm.Milestone"}}}}},"tags":["pm"]}},"/purple-suite/pm/milestones/{id}":{"get":{"summary":"Get milestone","description":"Fetch one Purple Planner milestone by id with its project, due date, status and completedAt stamp.","parameters":[{"$ref":"#/components/parameters/pm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pm.Milestone"}}}},"404":{"description":"Not found"}},"tags":["pm"]},"patch":{"summary":"Update milestone","description":"Partially update a Purple Planner milestone — move the due date, or flag it at_risk / in_progress / completed as delivery confidence changes.","parameters":[{"$ref":"#/components/parameters/pm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pm.Milestone"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pm.Milestone"}}}},"404":{"description":"Not found"}},"tags":["pm"]},"delete":{"summary":"Delete milestone","description":"Permanently delete a Purple Planner milestone. Destructive and irreversible — if the checkpoint simply slipped or was met, update its dueDate or status instead.","parameters":[{"$ref":"#/components/parameters/pm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["pm"]}},"/purple-suite/pm/initiatives":{"get":{"summary":"List initiatives","description":"Purple Planner initiatives are the portfolio layer above projects (e.g. \"26Q2 Platform Investments\"), which projects roll up to via Project.initiativeIds. Use these for portfolio/quarterly-theme questions; the delivery work itself lives in projects and tasks. Filter/sort server-side with $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send these as _filter, _orderby, …) rather than listing everything and filtering locally.","parameters":[{"$ref":"#/components/parameters/pm.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/pm.Initiative"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["pm"]},"post":{"summary":"Create initiative","description":"Create a cross-project initiative in Purple Planner to group related projects under one portfolio theme and owner.","parameters":[{"$ref":"#/components/parameters/pm.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/pm.Initiative"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pm.Initiative"}}}}},"tags":["pm"]}},"/purple-suite/pm/initiatives/{id}":{"get":{"summary":"Get initiative","description":"Fetch one Purple Planner initiative by id (e.g. INIT-001) with its owner, status and target date. Find the projects underneath it by filtering projects on initiativeIds.","parameters":[{"$ref":"#/components/parameters/pm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pm.Initiative"}}}},"404":{"description":"Not found"}},"tags":["pm"]},"patch":{"summary":"Update initiative","description":"Partially update a Purple Planner initiative — rename it, change owner, target date or status (planned | active | completed).","parameters":[{"$ref":"#/components/parameters/pm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pm.Initiative"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pm.Initiative"}}}},"404":{"description":"Not found"}},"tags":["pm"]},"delete":{"summary":"Delete initiative","description":"Permanently delete a Purple Planner initiative. Destructive and irreversible, and projects keep a dangling id in initiativeIds — mark it completed instead when the portfolio theme simply ends.","parameters":[{"$ref":"#/components/parameters/pm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["pm"]}},"/purple-suite/pm/labels":{"get":{"summary":"List labels","description":"Purple Planner labels are the tag vocabulary applied to tasks via Task.labelIds (e.g. \"p1\", \"tech-debt\"), each with a name, colour and description. Resolve a label name to its id here before filtering tasks by label. Filter/sort server-side with $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send these as _filter, _orderby, …) rather than listing everything and filtering locally.","parameters":[{"$ref":"#/components/parameters/pm.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/pm.Label"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["pm"]},"post":{"summary":"Create label","description":"Add a new label to the Purple Planner tag vocabulary. Check the existing labels first — this creates a new tag rather than applying one; applying is done by setting labelIds on a task.","parameters":[{"$ref":"#/components/parameters/pm.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/pm.Label"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pm.Label"}}}}},"tags":["pm"]}},"/purple-suite/pm/labels/{id}":{"get":{"summary":"Get label","description":"Fetch one Purple Planner label by id with its name, colour and description.","parameters":[{"$ref":"#/components/parameters/pm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pm.Label"}}}},"404":{"description":"Not found"}},"tags":["pm"]},"patch":{"summary":"Update label","description":"Rename or recolour a Purple Planner label. Tasks referencing it by id pick the change up automatically.","parameters":[{"$ref":"#/components/parameters/pm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pm.Label"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pm.Label"}}}},"404":{"description":"Not found"}},"tags":["pm"]},"delete":{"summary":"Delete label","description":"Permanently delete a Purple Planner label from the workspace vocabulary. Destructive — tasks keep the stale id in labelIds; to untag a single task, patch that task's labelIds instead.","parameters":[{"$ref":"#/components/parameters/pm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["pm"]}},"/purple-suite/pm/users":{"get":{"summary":"List workspace members","description":"Purple Planner workspace members are the people who can be a task assignee or project owner in this tool. This is not the chat roster, not the HR employee record, and not the canonical identity directory; use it to resolve an assignee or owner within Purple Planner. Filter/sort server-side with $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send these as _filter, _orderby, …) rather than listing everything and filtering locally.","parameters":[{"$ref":"#/components/parameters/pm.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/pm.User"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["pm"]},"post":{"summary":"Add workspace member","description":"Add a person to the Purple Planner workspace so they can be assigned issues or own projects. This creates a project-tool membership only — it does not create an HR employee record or an identity account.","parameters":[{"$ref":"#/components/parameters/pm.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/pm.User"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pm.User"}}}}},"tags":["pm"]}},"/purple-suite/pm/users/{id}":{"get":{"summary":"Get workspace member","description":"Fetch one Purple Planner workspace member by id (name, email, role) — the usual way to turn an assignee or owner reference into a person.","parameters":[{"$ref":"#/components/parameters/pm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pm.User"}}}},"404":{"description":"Not found"}},"tags":["pm"]},"patch":{"summary":"Update workspace member","description":"Update a Purple Planner member's name, email or role. Affects this project tool only, not their HR record or chat account.","parameters":[{"$ref":"#/components/parameters/pm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pm.User"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pm.User"}}}},"404":{"description":"Not found"}},"tags":["pm"]},"delete":{"summary":"Remove workspace member","description":"Remove a member from the Purple Planner workspace. Destructive and irreversible; their tasks and projects keep the id as a dangling assignee/owner, so reassign that work first.","parameters":[{"$ref":"#/components/parameters/pm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["pm"]}},"/purple-suite/pm/status_updates":{"get":{"summary":"List status updates","description":"Purple Planner status updates are narrative progress posts written against a project or initiative, each with a health flag (on_track | at_risk | off_track). Use these for \"what's the latest on X\" questions; numeric progress lives on the project record. Filter/sort server-side with $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send these as _filter, _orderby, …) rather than listing everything and filtering locally.","parameters":[{"$ref":"#/components/parameters/pm.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/pm.StatusUpdate"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["pm"]},"post":{"summary":"Post status update","description":"Post a narrative progress update against a Purple Planner project or initiative, with body text and a health flag. Use this to communicate status; use update-project to change the project's own status field.","parameters":[{"$ref":"#/components/parameters/pm.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/pm.StatusUpdate"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pm.StatusUpdate"}}}}},"tags":["pm"]}},"/purple-suite/pm/status_updates/{id}":{"get":{"summary":"Get status update","description":"Fetch one Purple Planner status update by id with its author, body, health flag and the project or initiative it covers.","parameters":[{"$ref":"#/components/parameters/pm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pm.StatusUpdate"}}}},"404":{"description":"Not found"}},"tags":["pm"]},"patch":{"summary":"Update status update","description":"Edit a previously posted Purple Planner status update — correct the body text or revise the health flag.","parameters":[{"$ref":"#/components/parameters/pm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pm.StatusUpdate"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pm.StatusUpdate"}}}},"404":{"description":"Not found"}},"tags":["pm"]},"delete":{"summary":"Delete status update","description":"Permanently delete a Purple Planner status update, removing it from the project's update history. Destructive and irreversible.","parameters":[{"$ref":"#/components/parameters/pm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["pm"]}},"/purple-suite/pm/teams":{"get":{"summary":"List teams","description":"Purple Planner teams are the delivery squads that own projects (Project.teamIds) and issues (Task.teamId), each with a short key such as PLAT and a memberIds roster. These are product/engineering squads, not HR departments or chat channels. Filter/sort server-side with $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send these as _filter, _orderby, …) rather than listing everything and filtering locally.","parameters":[{"$ref":"#/components/parameters/pm.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/pm.Team"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["pm"]},"post":{"summary":"Create team","description":"Create a delivery squad in Purple Planner with a short key (e.g. PLAT) and member roster, so projects and issues can be assigned to it.","parameters":[{"$ref":"#/components/parameters/pm.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/pm.Team"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pm.Team"}}}}},"tags":["pm"]}},"/purple-suite/pm/teams/{id}":{"get":{"summary":"Get team","description":"Fetch one Purple Planner team by id with its name, key and memberIds roster.","parameters":[{"$ref":"#/components/parameters/pm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pm.Team"}}}},"404":{"description":"Not found"}},"tags":["pm"]},"patch":{"summary":"Update team","description":"Rename a Purple Planner team, change its key, or add/remove people by rewriting memberIds.","parameters":[{"$ref":"#/components/parameters/pm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pm.Team"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pm.Team"}}}},"404":{"description":"Not found"}},"tags":["pm"]},"delete":{"summary":"Delete team","description":"Permanently delete a Purple Planner team. Destructive and irreversible, and projects/tasks keep a dangling team reference — reassign their ownership first.","parameters":[{"$ref":"#/components/parameters/pm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["pm"]}},"/purple-suite/purple-talent/search_people":{"post":{"summary":"Parse a free-text candidate search into criteria and return a search_parameters_ref (search_people)","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/purple-talent.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"query":{"type":"string"}},"additionalProperties":true},"example":{"query":"Senior product managers in New York with B2B SaaS experience"}}}},"responses":{"200":{"description":"Parse a free-text candidate search into criteria and return a search_parameters_ref (search_people)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true},"example":{"success":true,"search_parameters_ref":"ref_...","mode":"generate"}}}},"400":{"description":"Bad request"}},"tags":["purple-talent"]}},"/purple-suite/purple-talent/estimate_people_pool":{"post":{"summary":"Return the candidate pool size for a search_parameters_ref (estimate_people_pool)","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/purple-talent.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"search_parameters_ref":{"type":"string"},"data_source":{"type":"string"}},"additionalProperties":true},"example":{"search_parameters_ref":"ref_from_search_people","data_source":"public_profiles"}}}},"responses":{"200":{"description":"Return the candidate pool size for a search_parameters_ref (estimate_people_pool)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true},"example":{"success":true,"pool_size":1240}}}},"400":{"description":"Bad request"}},"tags":["purple-talent"]}},"/purple-suite/purple-talent/analyze_people_pool":{"post":{"summary":"Return a term-frequency breakdown (e.g. skills, titles) over a search_parameters_ref's pool (analyze_people_pool)","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/purple-talent.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"search_parameters_ref":{"type":"string"},"fields":{"type":"array"},"field_analysis_limit":{"type":"number"},"data_source":{"type":"string"}},"additionalProperties":true},"example":{"search_parameters_ref":"ref_from_search_people","fields":["skills","titles"],"field_analysis_limit":10,"data_source":"public_profiles"}}}},"responses":{"200":{"description":"Return a term-frequency breakdown (e.g. skills, titles) over a search_parameters_ref's pool (analyze_people_pool)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true},"example":{"success":true,"analysis_results":{"skills":[{"term":"Machine Learning","count":320,"percentage":25.8}]},"total_candidates_count":1240}}}},"400":{"description":"Bad request"}},"tags":["purple-talent"]}},"/purple-suite/purple-talent/analyze_company_role_flow":{"post":{"summary":"Return hire/departure headline + top sources for a company + role scope, synchronously (analyze_company_role_flow)","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/purple-talent.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"target_company":{"type":"string"},"direction":{"type":"string"},"role_scope":{"type":"string"},"title_keywords":{"type":"array"},"window":{"type":"object"},"top_n":{"type":"number"}},"additionalProperties":true},"example":{"target_company":"ExampleCo","direction":"both","role_scope":"title_keywords","title_keywords":["software engineer"],"window":{"lookback_months":18},"top_n":10}}}},"responses":{"200":{"description":"Return hire/departure headline + top sources for a company + role scope, synchronously (analyze_company_role_flow)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true},"example":{"success":true,"headline":{"inflow_joined_in_window_count":120,"outflow_left_in_window_count":90,"net_window_flow":30},"top_inflow_sources":[{"company_name":"SourceCo","count":18,"percentage":17.1}],"count_precision":{"mode":"exact_validated"}}}}},"400":{"description":"Bad request"}},"tags":["purple-talent"]}},"/purple-suite/purple-talent/count_global_jobs":{"post":{"summary":"Count global job postings matching company/query/location/status filters (count_global_jobs)","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/purple-talent.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"company_queries":{"type":"array"},"query":{"type":"string"},"location_query":{"type":"string"},"status_codes":{"type":"array"}},"additionalProperties":true},"example":{"company_queries":["OpenAI"],"query":"engineer","location_query":"New York","status_codes":[0]}}}},"responses":{"200":{"description":"Count global job postings matching company/query/location/status filters (count_global_jobs)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true},"example":{"success":true,"filters":{"query":"engineer","network_ids":[],"company_ids":[42],"location_query":"New York","status_codes":[0]},"total_count":27}}}},"400":{"description":"Bad request"}},"tags":["purple-talent"]}},"/purple-suite/purple-talent/global_jobs_by_query":{"post":{"summary":"Paginated search over the global job-postings corpus (global_jobs_by_query)","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/purple-talent.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"query":{"type":"string"},"filters":{"type":"object"},"page":{"type":"number"},"hits_per_page":{"type":"number"},"strict_filter_validation":{"type":"boolean"}},"additionalProperties":true},"example":{"query":"AI engineer machine learning","filters":{"status":"active","work_mode":"remote"},"page":0,"hits_per_page":20,"strict_filter_validation":true}}}},"responses":{"200":{"description":"Paginated search over the global job-postings corpus (global_jobs_by_query)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true},"example":{"success":true,"tool":"global_jobs_by_query","jobs":[{"id":123,"title":"Staff Machine Learning Engineer","organization":{"name":"Example Co"},"work_mode":"remote","status":"active"}],"total_count":84,"page":0,"hits_per_page":20,"total_pages":5,"meta":{"filters":{"status":"active","work_mode":"remote"},"unknown_filter_keys":[],"strict_filter_validation":true}}}}},"400":{"description":"Bad request"}},"tags":["purple-talent"]}},"/purple-suite/purple-talent/find_tools":{"post":{"summary":"Search for available tools by name or description (find_tools)","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/purple-talent.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"query":{"type":"string"}},"additionalProperties":true},"example":{"query":"people"}}}},"responses":{"200":{"description":"Search for available tools by name or description (find_tools)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true},"example":{"success":true,"query":"people","tools":[{"name":"search_people","description":"Parse a free-text candidate search","category":"discovery","input_schema":{}}]}}}},"400":{"description":"Bad request"}},"tags":["purple-talent"]}},"/purple-suite/purple-talent/list_skills":{"post":{"summary":"Get the skill taxonomy and available skills (list_skills)","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/purple-talent.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"limit":{"type":"number"}},"additionalProperties":true},"example":{"limit":100}}}},"responses":{"200":{"description":"Get the skill taxonomy and available skills (list_skills)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true},"example":{"success":true,"skills":[{"skill_id":"skill-product-strategy","name":"Product Strategy","description":"Strategic product planning and vision","builtIn":true}]}}}},"400":{"description":"Bad request"}},"tags":["purple-talent"]}},"/purple-suite/purple-talent/load_skill":{"post":{"summary":"Load details for a specific skill (load_skill)","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/purple-talent.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"skill_id":{"type":"string"}},"additionalProperties":true},"example":{"skill_id":"skill-product-strategy"}}}},"responses":{"200":{"description":"Load details for a specific skill (load_skill)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true},"example":{"success":true,"skill_id":"skill-product-strategy","name":"Product Strategy","instructions":"Product Strategy — Strategic product planning and vision","references":[{"ref_id":"skill-roadmapping","description":"Related skill"}]}}}},"400":{"description":"Bad request"}},"tags":["purple-talent"]}},"/purple-suite/purple-talent/resolve_company":{"post":{"summary":"Resolve company information by name or domain (resolve_company)","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/purple-talent.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"companies_info":{"type":"array"}},"additionalProperties":true},"example":{"companies_info":[{"name":"Acme Corp"}]}}}},"responses":{"200":{"description":"Resolve company information by name or domain (resolve_company)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true},"example":{"success":true,"resolved_count":1,"total_requested":1,"companies":[{"index":0,"resolved":true,"company_name":"Acme Corp","domain":"acme.com","industry_sub_categories":["Software"],"curr_comp_size":{"count":5200}}]}}}},"400":{"description":"Bad request"}},"tags":["purple-talent"]}},"/purple-suite/purple-talent/scout_people_pool":{"post":{"summary":"Advanced people pool search with filtering (scout_people_pool)","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/purple-talent.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"query":{"type":"string"},"company_meta_refs":{"type":"array"}},"additionalProperties":true},"example":{"query":"Senior product managers in New York","company_meta_refs":["acme-corp"]}}}},"responses":{"200":{"description":"Advanced people pool search with filtering (scout_people_pool)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true},"example":{"success":true,"search_parameters_ref":"SPREF-1691234567890","pool_size":47,"active_sections":["titles","companies"],"analysis":{"titles":[{"term":"Senior Product Manager","count":9,"percentage":19.1}],"companies":[]},"suggested_refinements":["Add skills filter","Filter by seniority"],"followup":{"type":"followup","subtype":"select","message":"How do you want to shape this pool?","options":["Show candidates","Show insights"],"actions":["Show candidates","Show insights"],"context":"discovery_next_step","search_parameters_ref":"SPREF-1691234567890","pool_size":47}}}}},"400":{"description":"Bad request"}},"tags":["purple-talent"]}},"/purple-suite/purple-talent/talent_pool_deep_research":{"post":{"summary":"Comprehensive talent pool analysis (talent_pool_deep_research)","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/purple-talent.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"icp_id":{"type":"string"}},"additionalProperties":true},"example":{"icp_id":"icp-001"}}}},"responses":{"200":{"description":"Comprehensive talent pool analysis (talent_pool_deep_research)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true},"example":{"success":true,"project_id":"proj-1691234567890","source_kind":"talent_pool","insights_link":"https://app.purpletalent.local/projects/proj-1691234567890","narrative_summary":"Talent pool analysis complete with 47 profiles analyzed."}}}},"400":{"description":"Bad request"}},"tags":["purple-talent"]}},"/purple-suite/purple-talent/preview_people":{"post":{"summary":"Fetch individual candidate profiles (preview_people)","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/purple-talent.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"search_parameters_ref":{"type":"string"},"limit":{"type":"number"}},"additionalProperties":true},"example":{"search_parameters_ref":"SPREF-...","limit":10}}}},"responses":{"200":{"description":"Fetch individual candidate profiles (preview_people)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true},"example":{"success":true,"total_matches":47,"candidates_table":{"id":"SPREF-...","title":"Candidate Pool","columns":[{"key":"name","title":"Name"},{"key":"title","title":"Title"},{"key":"location","title":"Location"},{"key":"skills","title":"Skills"}],"rows":[{"__candidate_id":"FP001","name":"John Doe","title":"Senior Product Manager","location":"New York, NY","skills":"Strategy, Roadmapping"}],"enable_enriched_profile_on_click":true,"allow_multiple_selections_for_actions":true},"candidates_table_markdown":"| Name | Title | Location |\n|------|-------|----------|","candidates_evaluated":10,"candidates_below_threshold":37}}}},"400":{"description":"Bad request"}},"tags":["purple-talent"]}},"/purple-suite/purple-talent/company_metrics":{"post":{"summary":"Get aggregated company statistics (company_metrics)","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/purple-talent.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"company_id":{"type":"string"}},"additionalProperties":true},"example":{"company_id":"acme-corp"}}}},"responses":{"200":{"description":"Get aggregated company statistics (company_metrics)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true},"example":{"success":true,"company_metrics":{"company_id":"acme-corp","summary":{"current_employee_count":5200,"current_leadership_count":520,"distribution_by_experience":[{"category":"5+ years","employee_count":2080,"leadership_count":520}],"distribution_by_tenure":[{"category":"3+ years","employee_count":2600,"leadership_count":520}],"distribution_by_department":[{"category":"Engineering","employee_count":1820,"leadership_count":104}]},"timeline":[{"year":2026,"employee_count":5200,"employee_hired":8,"employee_left":3}]}}}}},"400":{"description":"Bad request"}},"tags":["purple-talent"]}},"/purple-suite/purple-talent/analyze_company_workforce_trend":{"post":{"summary":"Time-series workforce trend analysis (analyze_company_workforce_trend)","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/purple-talent.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"company":{"type":"string"},"year_start":{"type":"number"},"year_end":{"type":"number"}},"additionalProperties":true},"example":{"company":"Acme Corp","year_start":2024,"year_end":2026}}}},"responses":{"200":{"description":"Time-series workforce trend analysis (analyze_company_workforce_trend)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true},"example":{"success":true,"company":{"company_id":"acme-corp","company_name":"Acme Corp"},"year_start":2024,"year_end":2026,"mode":"active","data_points":[{"year":2024,"total":18},{"year":2025,"total":25},{"year":2026,"total":8}],"yoy_changes":[{"from":2024,"to":2025,"delta":7,"delta_pct":38.9}],"total_growth_pct":-55.6,"chart_spec":{"chart_type":"line","title":"Workforce Trend (2024-2026)","series":[{"name":"Count","data":[18,25,8]}],"x_axis":{"name":"Year","data":[2024,2025,2026]},"y_axis":{"name":"Count","type":"value"}},"kpi_spec":{"title":"Key Metrics (active)","metrics":[{"label":"Total Growth %","value":"-55.6%"},{"label":"Latest Year Total","value":8}]}}}}},"400":{"description":"Bad request"}},"tags":["purple-talent"]}},"/purple-suite/purple-talent/web_search":{"post":{"summary":"Search the web for information (web_search)","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/purple-talent.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"query":{"type":"string"},"max_searches":{"type":"number"}},"additionalProperties":true},"example":{"query":"talent intelligence market trends","max_searches":2}}}},"responses":{"200":{"description":"Search the web for information (web_search)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true},"example":{"query":"talent intelligence market trends","answer":"Top findings: This topic has seen increased focus, with trends pointing toward increased investment and adoption.","findings":[{"claim":"Research shows strong market growth in 2026.","source_url":"https://example.com/article-1"},{"claim":"Key players are investing in talent acquisition.","source_url":"https://example.com/article-2"}],"sources":[{"url":"https://example.com/article-1","title":"Industry Insights"},{"url":"https://example.com/article-2","title":"Market Report"}],"model":"mock-web-search-v1","usage":{"searches_performed":2,"results_returned":2},"execution_mode":"interactive"}}}},"400":{"description":"Bad request"}},"tags":["purple-talent"]}},"/purple-suite/purple-talent/create_artifact":{"post":{"summary":"Create and store an analysis artifact (create_artifact)","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/purple-talent.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"artifact_type":{"type":"string"},"data":{"type":"object"}},"additionalProperties":true},"example":{"artifact_type":"talent_analysis","data":{"title":"PM Talent Pool Analysis","insights":[],"metadata":{}}}}}},"responses":{"200":{"description":"Create and store an analysis artifact (create_artifact)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true},"example":{"success":true,"artifact_id":"ART-abcd1234"}}}},"400":{"description":"Bad request"}},"tags":["purple-talent"]}},"/purple-suite/purple-talent/create_project_from_search_parameters":{"post":{"summary":"Create a project from search parameters (create_project_from_search_parameters)","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/purple-talent.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"search_parameters_ref":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"}},"additionalProperties":true},"example":{"search_parameters_ref":"SPREF-...","name":"Senior PM Search","description":"Search for senior PMs"}}}},"responses":{"200":{"description":"Create a project from search parameters (create_project_from_search_parameters)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true},"example":{"success":true,"project_id":"PROJ-xyz789","project_link":"https://app.purpletalent.local/searches/inst-123?joid=PROJ-xyz789&redirect_joid=PROJ-xyz789","scorecard_ref":"SC-123"}}}},"400":{"description":"Bad request"}},"tags":["purple-talent"]}},"/purple-suite/purple-talent/studio_list_workflows":{"post":{"summary":"List available workflow definitions (studio_list_workflows)","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/purple-talent.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"runnable_only":{"type":"boolean"}},"additionalProperties":true},"example":{"runnable_only":false}}}},"responses":{"200":{"description":"List available workflow definitions (studio_list_workflows)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true},"example":{"success":true,"workflows":[{"workflow_id":"wf_mqzeybg6","name":"Succession Planning Workflow","kind":"workflow","visibility":"internal","inputs":[{"key":"company","label":"Company"}],"externally_runnable":true}],"count":2,"render_templates":[]}}}},"400":{"description":"Bad request"}},"tags":["purple-talent"]}},"/purple-suite/purple-talent/studio_run_workflow":{"post":{"summary":"Execute a workflow asynchronously (studio_run_workflow)","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/purple-talent.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"workflow_key":{"type":"string"},"inputs":{"type":"object"}},"additionalProperties":true},"example":{"workflow_key":"wf__succession_planning_workflow_fbf487","inputs":{"company":"Acme Corp"}}}}},"responses":{"200":{"description":"Execute a workflow asynchronously (studio_run_workflow)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true},"example":{"success":true,"run_id":"RUN-abc123","status":"complete","workflow_name":"Succession Planning Workflow"}}}},"400":{"description":"Bad request"}},"tags":["purple-talent"]}},"/purple-suite/purple-talent/studio_get_run_status":{"post":{"summary":"Poll the status of an executing workflow (studio_get_run_status)","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/purple-talent.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"run_id":{"type":"string"}},"required":["run_id"]},"example":{"run_id":"RUN-abc123"}}}},"responses":{"200":{"description":"Poll the status of an executing workflow","content":{"application/json":{"schema":{"type":"object"},"example":{"success":true,"run_id":"RUN-abc123","status":"complete"}}}}},"tags":["purple-talent"]}},"/purple-suite/purple-talent/wf__succession_planning_workflow_fbf487":{"post":{"summary":"Execute Succession Planning Workflow","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/purple-talent.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"inputs":{"type":"object"}}},"example":{"inputs":{"company":"Acme Corp","department":"Engineering"}}}}},"responses":{"200":{"description":"Succession Planning Workflow execution result","content":{"application/json":{"schema":{"type":"object"},"example":{"success":true,"run_id":"RUN-fbf487abc","status":"complete"}}}}},"tags":["purple-talent"]}},"/purple-suite/purple-talent/wf__role_talent_inflow_outflow_analysis_e80ffb":{"post":{"summary":"Execute Role Talent Inflow/Outflow Analysis","x-mcp":false,"x-odata":false,"parameters":[{"$ref":"#/components/parameters/purple-talent.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"inputs":{"type":"object"}}},"example":{"inputs":{"company":"Acme Corp","role":"engineer","timeframe":"12"}}}}},"responses":{"200":{"description":"Role Talent Inflow/Outflow Analysis execution result","content":{"application/json":{"schema":{"type":"object"},"example":{"success":true,"run_id":"RUN-e80ffbabc","status":"complete"}}}}},"tags":["purple-talent"]}},"/purple-suite/retail-banking/customers":{"get":{"summary":"List banking customers","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Max items per page (default 50)"},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Pass the previous response's nextCursor to fetch the next page"},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."},{"in":"query","name":"country","required":false,"schema":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"description":"Filter to a single country (IN | AU | US)"},{"in":"query","name":"segment","required":false,"schema":{"type":"string","enum":["mass","affluent","hni","nri","sme"]},"description":"Filter by customer segment"},{"in":"query","name":"kyc_status","required":false,"schema":{"type":"string","enum":["verified","pending","expired","in_remediation"]},"description":"Filter by KYC status"},{"in":"query","name":"risk_rating","required":false,"schema":{"type":"string","enum":["low","medium","high"]},"description":"Filter by risk rating"},{"in":"query","name":"relationship_manager","required":false,"schema":{"type":"string"},"description":"Filter by relationship manager email"}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/retail-banking.Customer"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page"},"total":{"type":"integer","description":"Total documents matching the filter"}}}}}}},"description":"Purple Retail Banking customers are the retail parties (individuals and SMEs) who hold the accounts, cards and loans in this bank, each with a segment, KYC status, risk rating, relationship manager and country (IN | AU | US). Not CRM accounts or contacts (B2B sales) and not internal employees. Narrow results server-side with this endpoint's named filter params (country, customer_id, status, …) or with OData $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send those as _filter, _orderby, …) instead of paging through everything.","tags":["retail-banking"]},"post":{"summary":"Onboard customer","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Customer"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Customer"}}}}},"description":"Create a new retail banking customer record. Seldom the right move during a servicing conversation: look the person up with the list tool first, since duplicate customers orphan accounts and cards.","tags":["retail-banking"]}},"/purple-suite/retail-banking/customers/{id}":{"get":{"summary":"Get banking customer","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Customer"}}}},"404":{"description":"Not found"}},"description":"Fetch one banking customer by id (e.g. CUST-0001) with segment, KYC status, risk rating, credit score, relationship manager and home branch. To get their accounts, cards, loans and open complaints in a single call, use the customer portfolio tool instead.","tags":["retail-banking"]},"patch":{"summary":"Update banking customer","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Customer"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Customer"}}}},"404":{"description":"Not found"}},"description":"Partially update a banking customer's profile — contact details, address, segment, relationship manager or risk rating. KYC remediation and watchlist handling live in Purple Risk & Compliance, not here.","tags":["retail-banking"]},"delete":{"summary":"Delete banking customer","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"id":{"type":"string"}}}}}},"404":{"description":"Not found"}},"description":"Permanently delete a banking customer record. Destructive and irreversible, and it orphans their accounts, cards, loans and complaints — effectively never correct in a servicing flow.","tags":["retail-banking"]}},"/purple-suite/retail-banking/customers/{id}/portfolio":{"get":{"summary":"Get customer 360 portfolio","description":"One-call briefing on a single banking customer: the customer record plus their accounts, cards, loans, opportunities, recent interactions and open complaints. Prefer this over five separate list calls whenever the question is about one named customer.","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Aggregated customer portfolio","content":{"application/json":{"schema":{"type":"object","properties":{"customer":{"$ref":"#/components/schemas/retail-banking.Customer"},"accounts":{"type":"array","items":{"$ref":"#/components/schemas/retail-banking.Account"}},"cards":{"type":"array","items":{"$ref":"#/components/schemas/retail-banking.Card"}},"loans":{"type":"array","items":{"$ref":"#/components/schemas/retail-banking.Loan"}},"opportunities":{"type":"array","items":{"$ref":"#/components/schemas/retail-banking.Opportunity"}},"recent_interactions":{"type":"array","items":{"$ref":"#/components/schemas/retail-banking.Interaction"}},"open_complaints":{"type":"array","items":{"$ref":"#/components/schemas/retail-banking.Complaint"}}}}}}},"404":{"description":"Customer not found"}},"tags":["retail-banking"]}},"/purple-suite/retail-banking/bank_accounts":{"get":{"summary":"List bank accounts","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Max items per page (default 50)"},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Pass the previous response's nextCursor to fetch the next page"},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."},{"in":"query","name":"country","required":false,"schema":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"description":"Filter to a single country (IN | AU | US)"},{"in":"query","name":"customer_id","required":false,"schema":{"type":"string"},"description":"Filter by customer id"},{"in":"query","name":"type","required":false,"schema":{"type":"string"},"description":"Filter by account type"},{"in":"query","name":"status","required":false,"schema":{"type":"string","enum":["active","dormant","frozen","closed"]},"description":"Filter by status"}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/retail-banking.Account"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page"},"total":{"type":"integer","description":"Total documents matching the filter"}}}}}}},"description":"Deposit and current accounts held by Purple Retail Banking customers, with a self-describing balance, status (active | dormant | frozen | closed), branch and routing details. These are real bank accounts, not CRM accounts (companies) or ERP ledger accounts. Narrow results server-side with this endpoint's named filter params (country, customer_id, status, …) or with OData $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send those as _filter, _orderby, …) instead of paging through everything.","tags":["retail-banking"]},"post":{"summary":"Open bank account","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Account"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Account"}}}}},"description":"Create a new deposit or current account for an existing banking customer. Use this to open an account, not to record a payment into one.","tags":["retail-banking"]}},"/purple-suite/retail-banking/bank_accounts/{id}":{"get":{"summary":"Get bank account","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Account"}}}},"404":{"description":"Not found"}},"description":"Fetch one bank account by id (e.g. ACCT-0001) with its balance, status, freeze_reason, routing details and status_history — the record to check before freezing or explaining a decline.","tags":["retail-banking"]},"patch":{"summary":"Update bank account","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Account"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Account"}}}},"404":{"description":"Not found"}},"description":"Partially update an account's mutable fields such as type, branch or routing. Do not change the status field here to stop activity: the freeze and unfreeze tools enforce the transition and write status_history.","tags":["retail-banking"]},"delete":{"summary":"Delete bank account","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"id":{"type":"string"}}}}}},"404":{"description":"Not found"}},"description":"Permanently delete a bank account record. Destructive and irreversible, and it strands the account's transactions and cards. To stop activity, freeze it; to close it properly, set status to 'closed'.","tags":["retail-banking"]}},"/purple-suite/retail-banking/bank_accounts/{id}/freeze":{"post":{"summary":"Freeze a bank account","description":"Stop all activity on a bank account: sets status to 'frozen' and records the reason in status_history. Use it for suspected fraud or a customer request covering the whole account; to stop just one card use the card block tool instead. Fails with 422 if the account is already frozen or closed. Reversible via unfreeze.","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string","example":"Suspected unauthorised activity"}}}}}},"responses":{"200":{"description":"Frozen","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Account"}}}},"404":{"description":"Not found"},"422":{"description":"Invalid state transition"}},"tags":["retail-banking"]}},"/purple-suite/retail-banking/bank_accounts/{id}/unfreeze":{"post":{"summary":"Unfreeze a bank account","description":"Return a frozen bank account to 'active' and record why, e.g. once a fraud review clears. Fails with 422 if the account is not currently frozen.","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string"}}}}}},"responses":{"200":{"description":"Unfrozen","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Account"}}}},"404":{"description":"Not found"},"422":{"description":"Invalid state transition"}},"tags":["retail-banking"]}},"/purple-suite/retail-banking/transactions":{"get":{"summary":"List account transactions","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Max items per page (default 50)"},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Pass the previous response's nextCursor to fetch the next page"},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."},{"in":"query","name":"country","required":false,"schema":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"description":"Filter to a single country (IN | AU | US)"},{"in":"query","name":"account_id","required":false,"schema":{"type":"string"},"description":"Filter by account id"},{"in":"query","name":"direction","required":false,"schema":{"type":"string","enum":["debit","credit"]},"description":"Filter by direction"},{"in":"query","name":"channel","required":false,"schema":{"type":"string"},"description":"Filter by channel"},{"in":"query","name":"status","required":false,"schema":{"type":"string","enum":["posted","pending","disputed","reversed"]},"description":"Filter by status"}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/retail-banking.Transaction"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page"},"total":{"type":"integer","description":"Total documents matching the filter"}}}}}}},"description":"Money movement on Purple Retail Banking accounts — card, UPI and transfer entries with a self-describing amount, direction (debit | credit), channel, counterparty, status and an is_flagged marker. Customer banking activity, not ERP invoices or employee expenses. Narrow results server-side with this endpoint's named filter params (country, customer_id, status, …) or with OData $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send those as _filter, _orderby, …) instead of paging through everything.","tags":["retail-banking"]},"post":{"summary":"Create transaction","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Transaction"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Transaction"}}}}},"description":"Write a transaction record against an account. This is a source-system record only: it does not move money and does not recompute the account balance.","tags":["retail-banking"]}},"/purple-suite/retail-banking/transactions/{id}":{"get":{"summary":"Get transaction","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Transaction"}}}},"404":{"description":"Not found"}},"description":"Fetch one transaction by id (e.g. TXN-00001) with its amount, direction, channel, counterparty and status — the entry to cite when a customer queries or disputes a debit.","tags":["retail-banking"]},"patch":{"summary":"Update transaction","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Transaction"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Transaction"}}}},"404":{"description":"Not found"}},"description":"Partially update a transaction, typically to mark it disputed or reversed, or to set is_flagged during a fraud review. The fraud alert and case records themselves live in Purple Risk & Compliance.","tags":["retail-banking"]},"delete":{"summary":"Delete transaction","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"id":{"type":"string"}}}}}},"404":{"description":"Not found"}},"description":"Permanently delete a transaction record. Destructive and irreversible, and it breaks the account's audit trail — model a real reversal by setting status to 'reversed' instead.","tags":["retail-banking"]}},"/purple-suite/retail-banking/cards":{"get":{"summary":"List cards","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Max items per page (default 50)"},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Pass the previous response's nextCursor to fetch the next page"},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."},{"in":"query","name":"country","required":false,"schema":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"description":"Filter to a single country (IN | AU | US)"},{"in":"query","name":"customer_id","required":false,"schema":{"type":"string"},"description":"Filter by customer id"},{"in":"query","name":"account_id","required":false,"schema":{"type":"string"},"description":"Filter by account id"},{"in":"query","name":"status","required":false,"schema":{"type":"string","enum":["active","blocked","expired"]},"description":"Filter by status"}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/retail-banking.Card"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page"},"total":{"type":"integer","description":"Total documents matching the filter"}}}}}}},"description":"Debit and credit cards issued to Purple Retail Banking customers, with network, type, masked last_four, the account they draw on, limits and status (active | blocked | expired). Narrow results server-side with this endpoint's named filter params (country, customer_id, status, …) or with OData $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send those as _filter, _orderby, …) instead of paging through everything.","tags":["retail-banking"]},"post":{"summary":"Issue card","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Card"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Card"}}}}},"description":"Issue a new card to a banking customer and link it to one of their accounts. For a lost card, block the existing card first rather than only issuing a replacement.","tags":["retail-banking"]}},"/purple-suite/retail-banking/cards/{id}":{"get":{"summary":"Get card","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Card"}}}},"404":{"description":"Not found"}},"description":"Fetch one card by id with its network, type, last_four, linked account, limits and status — the record to check before blocking or explaining a declined payment.","tags":["retail-banking"]},"patch":{"summary":"Update card","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Card"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Card"}}}},"404":{"description":"Not found"}},"description":"Partially update card attributes such as limits, linked account or expiry. To stop a card, use the block tool rather than patching status: it enforces the transition and is reversible.","tags":["retail-banking"]},"delete":{"summary":"Delete card","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"id":{"type":"string"}}}}}},"404":{"description":"Not found"}},"description":"Permanently delete a card record. Destructive and irreversible — for a lost, stolen or compromised card, block it instead, which is reversible and preserves history.","tags":["retail-banking"]}},"/purple-suite/retail-banking/cards/{id}/block":{"post":{"summary":"Block a card","description":"Stop a single card by setting its status to 'blocked' — the usual response to lost, stolen or compromised plastic. Narrower than freezing the account: the account and the customer's other cards keep working. Fails with 422 if the card is already blocked or expired. Reversible via unblock.","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string"}}}}}},"responses":{"200":{"description":"Blocked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Card"}}}},"404":{"description":"Not found"},"422":{"description":"Invalid state transition"}},"tags":["retail-banking"]}},"/purple-suite/retail-banking/cards/{id}/unblock":{"post":{"summary":"Unblock a card","description":"Return a blocked card to 'active', e.g. after the customer finds it or a fraud review clears. Fails with 422 if the card is not currently blocked.","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Unblocked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Card"}}}},"404":{"description":"Not found"},"422":{"description":"Invalid state transition"}},"tags":["retail-banking"]}},"/purple-suite/retail-banking/products":{"get":{"summary":"List banking products","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Max items per page (default 50)"},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Pass the previous response's nextCursor to fetch the next page"},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."},{"in":"query","name":"country","required":false,"schema":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"description":"Filter to a single country (IN | AU | US)"},{"in":"query","name":"category","required":false,"schema":{"type":"string","enum":["deposit","lending","card","investment","insurance"]},"description":"Filter by category"},{"in":"query","name":"active","required":false,"schema":{"type":"string"},"description":"Filter by active flag"}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/retail-banking.Product"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page"},"total":{"type":"integer","description":"Total documents matching the filter"}}}}}}},"description":"The bank's retail product catalogue (deposit | lending | card | investment | insurance) with interest rate, min/max amounts, eligibility summary and per-country availability. Catalogue definitions — nothing here is held by a specific customer. Narrow results server-side with this endpoint's named filter params (country, customer_id, status, …) or with OData $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send those as _filter, _orderby, …) instead of paging through everything.","tags":["retail-banking"]},"post":{"summary":"Create banking product","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Product"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Product"}}}}},"description":"Add a product to the retail banking catalogue. This is not how a customer gets a product: that means opening an account, issuing a card, or booking a loan.","tags":["retail-banking"]}},"/purple-suite/retail-banking/products/{id}":{"get":{"summary":"Get banking product","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Product"}}}},"404":{"description":"Not found"}},"description":"Fetch one catalogue product by id with its rate, amount limits, eligibility summary and country availability — the source for 'what does this product require' answers.","tags":["retail-banking"]},"patch":{"summary":"Update banking product","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Product"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Product"}}}},"404":{"description":"Not found"}},"description":"Update a catalogue product's commercial terms — rate, limits, eligibility text — or clear its active flag when the product is withdrawn.","tags":["retail-banking"]},"delete":{"summary":"Delete banking product","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"id":{"type":"string"}}}}}},"404":{"description":"Not found"}},"description":"Permanently delete a product from the catalogue. Destructive — if customers hold it, set active=false instead so existing accounts and loans still resolve.","tags":["retail-banking"]}},"/purple-suite/retail-banking/loans":{"get":{"summary":"List loans","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Max items per page (default 50)"},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Pass the previous response's nextCursor to fetch the next page"},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."},{"in":"query","name":"country","required":false,"schema":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"description":"Filter to a single country (IN | AU | US)"},{"in":"query","name":"customer_id","required":false,"schema":{"type":"string"},"description":"Filter by customer id"},{"in":"query","name":"status","required":false,"schema":{"type":"string","enum":["active","delinquent","closed","written_off"]},"description":"Filter by status"},{"in":"query","name":"delinquency_bucket","required":false,"schema":{"type":"string","enum":["current","1-30","31-60","61-90","90+"]},"description":"Filter by delinquency bucket"}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/retail-banking.Loan"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page"},"total":{"type":"integer","description":"Total documents matching the filter"}}}}}}},"description":"Disbursed loans held by Purple Retail Banking customers, with principal and outstanding money, EMI, status (active | delinquent | closed | written_off), days past due and delinquency bucket. These are funded loans; applications still in origination are loan applications. Narrow results server-side with this endpoint's named filter params (country, customer_id, status, …) or with OData $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send those as _filter, _orderby, …) instead of paging through everything.","tags":["retail-banking"]},"post":{"summary":"Create loan","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Loan"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Loan"}}}}},"description":"Book a loan record for a customer. In the normal flow a loan comes into existence by disbursing an approved loan application, so check that pipeline before creating one directly.","tags":["retail-banking"]}},"/purple-suite/retail-banking/loans/{id}":{"get":{"summary":"Get loan","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Loan"}}}},"404":{"description":"Not found"}},"description":"Fetch one loan by id with its outstanding balance, EMI, dpd and delinquency bucket — the record behind payoff, arrears and collections questions.","tags":["retail-banking"]},"patch":{"summary":"Update loan","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Loan"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Loan"}}}},"404":{"description":"Not found"}},"description":"Partially update a loan — record repayment progress, refresh dpd and delinquency bucket, or change status as it cures or deteriorates.","tags":["retail-banking"]},"delete":{"summary":"Delete loan","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"id":{"type":"string"}}}}}},"404":{"description":"Not found"}},"description":"Permanently delete a loan record. Destructive and irreversible — a settled loan should be status 'closed' and an unrecoverable one 'written_off' so the credit history survives.","tags":["retail-banking"]}},"/purple-suite/retail-banking/loan_applications":{"get":{"summary":"List loan applications","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Max items per page (default 50)"},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Pass the previous response's nextCursor to fetch the next page"},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."},{"in":"query","name":"country","required":false,"schema":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"description":"Filter to a single country (IN | AU | US)"},{"in":"query","name":"customer_id","required":false,"schema":{"type":"string"},"description":"Filter by customer id"},{"in":"query","name":"stage","required":false,"schema":{"type":"string","enum":["submitted","docs_pending","eligibility","underwriting","approved","rejected","disbursed"]},"description":"Filter by stage"}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/retail-banking.LoanApplication"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page"},"total":{"type":"integer","description":"Total documents matching the filter"}}}}}}},"description":"Loan applications in origination, moving through submitted → docs_pending → eligibility → underwriting → approved → disbursed (or rejected), with requested amount, eligibility score and a required-document checklist. Nothing here is money lent yet — that is loans. Narrow results server-side with this endpoint's named filter params (country, customer_id, status, …) or with OData $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send those as _filter, _orderby, …) instead of paging through everything.","tags":["retail-banking"]},"post":{"summary":"Submit loan application","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.LoanApplication"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.LoanApplication"}}}}},"description":"Submit a new loan application for a customer against a lending product. It enters at the start of the pipeline; move it forward with the transition tool rather than by creating further applications.","tags":["retail-banking"]}},"/purple-suite/retail-banking/loan_applications/{id}":{"get":{"summary":"Get loan application","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.LoanApplication"}}}},"404":{"description":"Not found"}},"description":"Fetch one loan application by id with its current stage, eligibility score and per-document status — the check to run before advancing it or chasing paperwork.","tags":["retail-banking"]},"patch":{"summary":"Update loan application","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.LoanApplication"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.LoanApplication"}}}},"404":{"description":"Not found"}},"description":"Partially update application details such as requested amount, product or assigned underwriter. Do not set the stage field here: the transition tool enforces the pipeline, and document status has its own tool.","tags":["retail-banking"]},"delete":{"summary":"Delete loan application","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"id":{"type":"string"}}}}}},"404":{"description":"Not found"}},"description":"Permanently delete a loan application. Destructive and irreversible — a declined case should be transitioned to 'rejected' so the decision and its reason survive.","tags":["retail-banking"]}},"/purple-suite/retail-banking/loan_applications/{id}/documents":{"post":{"summary":"Set loan application document status","description":"Mark one required document on a loan application as missing, received or verified, adding it to the checklist if it is not already listed. This is the paperwork step that clears the docs_pending stage; it does not itself advance the application — use the transition tool for that.","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"doc_type":{"type":"string","example":"income_proof"},"status":{"type":"string","enum":["missing","received","verified"]}},"required":["doc_type","status"]}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.LoanApplication"}}}},"400":{"description":"Invalid body"},"404":{"description":"Not found"}},"tags":["retail-banking"]}},"/purple-suite/retail-banking/loan_applications/{id}/transition":{"post":{"summary":"Advance a loan application","description":"Move a loan application along the origination pipeline: submitted → docs_pending → eligibility → underwriting → approved → disbursed, with 'rejected' allowed from any pre-decision stage. Use this rather than patching the stage field: skipping or reversing stages is rejected with 422, and 'rejected' and 'disbursed' are terminal credit decisions that cannot be undone.","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"stage":{"type":"string","enum":["submitted","docs_pending","eligibility","underwriting","approved","rejected","disbursed"]},"reason":{"type":"string"}},"required":["stage"]}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.LoanApplication"}}}},"400":{"description":"Invalid stage"},"404":{"description":"Not found"},"422":{"description":"Invalid state transition"}},"tags":["retail-banking"]}},"/purple-suite/retail-banking/interactions":{"get":{"summary":"List customer interactions","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Max items per page (default 50)"},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Pass the previous response's nextCursor to fetch the next page"},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."},{"in":"query","name":"country","required":false,"schema":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"description":"Filter to a single country (IN | AU | US)"},{"in":"query","name":"customer_id","required":false,"schema":{"type":"string"},"description":"Filter by customer id"},{"in":"query","name":"type","required":false,"schema":{"type":"string","enum":["call","meeting","note","visit"]},"description":"Filter by type"}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/retail-banking.Interaction"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page"},"total":{"type":"integer","description":"Total documents matching the filter"}}}}}}},"description":"Servicing touchpoints logged with a banking customer — calls, meetings, notes and branch visits — with subject, channel and who handled it. Relationship history inside the bank; distinct from CRM activities (B2B sales) and from complaints. Narrow results server-side with this endpoint's named filter params (country, customer_id, status, …) or with OData $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send those as _filter, _orderby, …) instead of paging through everything.","tags":["retail-banking"]},"post":{"summary":"Log customer interaction","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Interaction"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Interaction"}}}}},"description":"Log a servicing touchpoint (call, meeting, note or visit) against a banking customer. Use this for a contact record; if the customer is raising a grievance, log a complaint instead.","tags":["retail-banking"]}},"/purple-suite/retail-banking/interactions/{id}":{"get":{"summary":"Get customer interaction","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Interaction"}}}},"404":{"description":"Not found"}},"description":"Fetch one logged interaction by id with its type, subject, notes, handler and timestamp.","tags":["retail-banking"]},"patch":{"summary":"Update customer interaction","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Interaction"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Interaction"}}}},"404":{"description":"Not found"}},"description":"Correct or extend a logged interaction — its subject, notes or outcome.","tags":["retail-banking"]},"delete":{"summary":"Delete customer interaction","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"id":{"type":"string"}}}}}},"404":{"description":"Not found"}},"description":"Permanently delete a logged interaction, removing it from the customer's contact history. Destructive and irreversible.","tags":["retail-banking"]}},"/purple-suite/retail-banking/bank_opportunities":{"get":{"summary":"List banking opportunities","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Max items per page (default 50)"},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Pass the previous response's nextCursor to fetch the next page"},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."},{"in":"query","name":"country","required":false,"schema":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"description":"Filter to a single country (IN | AU | US)"},{"in":"query","name":"customer_id","required":false,"schema":{"type":"string"},"description":"Filter by customer id"},{"in":"query","name":"product_category","required":false,"schema":{"type":"string","enum":["deposit","lending","card","investment","insurance"]},"description":"Filter by product category"},{"in":"query","name":"stage","required":false,"schema":{"type":"string","enum":["identified","proposed","accepted","declined"]},"description":"Filter by stage"}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/retail-banking.Opportunity"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page"},"total":{"type":"integer","description":"Total documents matching the filter"}}}}}}},"description":"Next-best-action product opportunities identified for a banking customer (deposit | lending | card | investment | insurance) with expected value, stage and a recommendation reason. Retail cross-sell prompts for a relationship manager, not CRM sales opportunities (B2B deals). Narrow results server-side with this endpoint's named filter params (country, customer_id, status, …) or with OData $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send those as _filter, _orderby, …) instead of paging through everything.","tags":["retail-banking"]},"post":{"summary":"Create banking opportunity","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Opportunity"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Opportunity"}}}}},"description":"Record a next-best-action product opportunity for a banking customer, with the product category, expected value and why it is being recommended.","tags":["retail-banking"]}},"/purple-suite/retail-banking/bank_opportunities/{id}":{"get":{"summary":"Get banking opportunity","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Opportunity"}}}},"404":{"description":"Not found"}},"description":"Fetch one banking opportunity by id with its product category, value, stage and recommendation reason.","tags":["retail-banking"]},"patch":{"summary":"Update banking opportunity","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Opportunity"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Opportunity"}}}},"404":{"description":"Not found"}},"description":"Move an opportunity along identified → proposed → accepted | declined, or revise its value and reasoning.","tags":["retail-banking"]},"delete":{"summary":"Delete banking opportunity","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"id":{"type":"string"}}}}}},"404":{"description":"Not found"}},"description":"Permanently delete a banking opportunity. Destructive — when a customer says no, set the stage to 'declined' instead so the outcome is retained.","tags":["retail-banking"]}},"/purple-suite/retail-banking/complaints":{"get":{"summary":"List customer complaints","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Max items per page (default 50)"},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Pass the previous response's nextCursor to fetch the next page"},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."},{"in":"query","name":"country","required":false,"schema":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"description":"Filter to a single country (IN | AU | US)"},{"in":"query","name":"customer_id","required":false,"schema":{"type":"string"},"description":"Filter by customer id"},{"in":"query","name":"category","required":false,"schema":{"type":"string","enum":["payments","cards","loans","accounts","atm","digital"]},"description":"Filter by category"},{"in":"query","name":"status","required":false,"schema":{"type":"string","enum":["logged","investigating","resolved","closed"]},"description":"Filter by status"}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/retail-banking.Complaint"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page"},"total":{"type":"integer","description":"Total documents matching the filter"}}}}}}},"description":"Grievances customers have raised against the bank (payments, cards, loans, accounts, atm, digital), each with status logged | investigating | resolved | closed and an SLA clock. Regulated banking complaints, not IT incidents and not fraud cases. Narrow results server-side with this endpoint's named filter params (country, customer_id, status, …) or with OData $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send those as _filter, _orderby, …) instead of paging through everything.","tags":["retail-banking"]},"post":{"summary":"Log customer complaint","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Complaint"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Complaint"}}}}},"description":"Log a new complaint a customer has raised against the bank, with category and description. It starts at 'logged'; move it on with the transition tool.","tags":["retail-banking"]}},"/purple-suite/retail-banking/complaints/{id}":{"get":{"summary":"Get customer complaint","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Complaint"}}}},"404":{"description":"Not found"}},"description":"Fetch one complaint by id with its category, status, resolution notes and SLA timing — the record to check before promising a resolution date.","tags":["retail-banking"]},"patch":{"summary":"Update customer complaint","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Complaint"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Complaint"}}}},"404":{"description":"Not found"}},"description":"Partially update complaint details such as category, description, owner or notes. Use the transition tool for the status lifecycle: it enforces the required order.","tags":["retail-banking"]},"delete":{"summary":"Delete customer complaint","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"id":{"type":"string"}}}}}},"404":{"description":"Not found"}},"description":"Permanently delete a complaint. Destructive and irreversible, and it erases regulated complaint history — resolve and close it instead.","tags":["retail-banking"]}},"/purple-suite/retail-banking/complaints/{id}/transition":{"post":{"summary":"Transition a complaint","description":"Move a customer complaint through logged → investigating → resolved → closed, optionally recording resolution notes. Use this rather than patching status: a complaint must be 'resolved' before it can be 'closed', and 'closed' is terminal — a closed complaint cannot be reopened.","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["logged","investigating","resolved","closed"]},"resolution_notes":{"type":"string"}},"required":["status"]}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.Complaint"}}}},"400":{"description":"Invalid status"},"404":{"description":"Not found"},"422":{"description":"Invalid state transition"}},"tags":["retail-banking"]}},"/purple-suite/retail-banking/known_issues":{"get":{"summary":"List banking known issues","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Max items per page (default 50)"},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Pass the previous response's nextCursor to fetch the next page"},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."},{"in":"query","name":"country","required":false,"schema":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"description":"Filter to a single country (IN | AU | US)"},{"in":"query","name":"status","required":false,"schema":{"type":"string","enum":["known_error","resolved"]},"description":"Filter by status"},{"in":"query","name":"affected_service","required":false,"schema":{"type":"string"},"description":"Filter by affected service"}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/retail-banking.KnownIssue"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page"},"total":{"type":"integer","description":"Total documents matching the filter"}}}}}}},"description":"Bank-wide known errors and service outages (e.g. a UPI/NPCI outage) with the affected service, customer-facing workaround and status. Check here before investigating one customer's failed payment — it is often already a known issue. Narrow results server-side with this endpoint's named filter params (country, customer_id, status, …) or with OData $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send those as _filter, _orderby, …) instead of paging through everything.","tags":["retail-banking"]},"post":{"summary":"Create banking known issue","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.KnownIssue"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.KnownIssue"}}}}},"description":"Publish a known issue or outage affecting a banking service, including the workaround agents should give customers.","tags":["retail-banking"]}},"/purple-suite/retail-banking/known_issues/{id}":{"get":{"summary":"Get banking known issue","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.KnownIssue"}}}},"404":{"description":"Not found"}},"description":"Fetch one known issue by id with the affected service, workaround, status and when it started.","tags":["retail-banking"]},"patch":{"summary":"Update banking known issue","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.KnownIssue"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.KnownIssue"}}}},"404":{"description":"Not found"}},"description":"Update a known issue — refine the workaround, or set status to 'resolved' once the service recovers.","tags":["retail-banking"]},"delete":{"summary":"Delete banking known issue","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"id":{"type":"string"}}}}}},"404":{"description":"Not found"}},"description":"Permanently delete a known issue record. Destructive — mark it 'resolved' instead so agents can still see that the outage happened.","tags":["retail-banking"]}},"/purple-suite/retail-banking/bank_sla_policies":{"get":{"summary":"List banking SLA policies","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Max items per page (default 50)"},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Pass the previous response's nextCursor to fetch the next page"},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."},{"in":"query","name":"country","required":false,"schema":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"description":"Filter to a single country (IN | AU | US)"},{"in":"query","name":"category","required":false,"schema":{"type":"string","enum":["payments","cards","loans","accounts","atm","digital"]},"description":"Filter by category"}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/retail-banking.SlaPolicy"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page"},"total":{"type":"integer","description":"Total documents matching the filter"}}}}}}},"description":"The bank's service-level commitments by country and category (payments, cards, loans, accounts, atm, digital), giving response and resolution hours and whether a regulatory clock applies. Use these to judge whether a complaint is breaching. Not ITSM SLAs. Narrow results server-side with this endpoint's named filter params (country, customer_id, status, …) or with OData $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send those as _filter, _orderby, …) instead of paging through everything.","tags":["retail-banking"]},"post":{"summary":"Create banking SLA policy","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.SlaPolicy"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.SlaPolicy"}}}}},"description":"Define a new banking service-level policy for a country and category, with response/resolution hours and the regulatory-clock flag.","tags":["retail-banking"]}},"/purple-suite/retail-banking/bank_sla_policies/{id}":{"get":{"summary":"Get banking SLA policy","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.SlaPolicy"}}}},"404":{"description":"Not found"}},"description":"Fetch one banking SLA policy by id with its response and resolution hours and regulatory-clock flag.","tags":["retail-banking"]},"patch":{"summary":"Update banking SLA policy","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.SlaPolicy"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/retail-banking.SlaPolicy"}}}},"404":{"description":"Not found"}},"description":"Change a banking SLA policy's response or resolution hours, or its regulatory-clock flag.","tags":["retail-banking"]},"delete":{"summary":"Delete banking SLA policy","parameters":[{"$ref":"#/components/parameters/retail-banking.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"id":{"type":"string"}}}}}},"404":{"description":"Not found"}},"description":"Permanently delete a banking SLA policy. Destructive — complaints in that category then have no target to measure against.","tags":["retail-banking"]}},"/purple-suite/risk-compliance/customers/{id}/risk-summary":{"get":{"summary":"Get customer risk summary","description":"One-call risk briefing on a single customer: their KYC profile, open remediation cases, fraud alerts, fraud cases and AML watchlist hits. Prefer this over several separate list calls when the question is whether one named customer is risky. The id is the shared retail-banking customer id (CUST-####).","parameters":[{"$ref":"#/components/parameters/risk-compliance.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Aggregated risk summary","content":{"application/json":{"schema":{"type":"object","properties":{"customer_id":{"type":"string"},"kyc_profile":{"$ref":"#/components/schemas/risk-compliance.KycProfile"},"open_remediation":{"type":"array","items":{"$ref":"#/components/schemas/risk-compliance.RemediationCase"}},"fraud_alerts":{"type":"array","items":{"$ref":"#/components/schemas/risk-compliance.FraudAlert"}},"fraud_cases":{"type":"array","items":{"$ref":"#/components/schemas/risk-compliance.FraudCase"}},"watchlist_hits":{"type":"array","items":{"$ref":"#/components/schemas/risk-compliance.WatchlistEntry"}}}}}}}},"tags":["risk-compliance"]}},"/purple-suite/risk-compliance/kyc_profiles":{"get":{"summary":"List KYC profiles","parameters":[{"$ref":"#/components/parameters/risk-compliance.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Max items per page (default 50)"},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Pass the previous response's nextCursor to fetch the next page"},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."},{"in":"query","name":"country","required":false,"schema":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"description":"Filter to a single country (IN | AU | US)"},{"in":"query","name":"customer_id","required":false,"schema":{"type":"string"},"description":"Filter by customer id"},{"in":"query","name":"status","required":false,"schema":{"type":"string","enum":["verified","pending","expired","in_remediation"]},"description":"Filter by KYC status"},{"in":"query","name":"risk_level","required":false,"schema":{"type":"string","enum":["low","medium","high"]},"description":"Filter by risk level"}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/risk-compliance.KycProfile"}},"nextCursor":{"type":"string","nullable":true},"total":{"type":"integer"}}}}}}},"description":"KYC/CDD profiles are the per-customer compliance record in Purple Risk & Compliance: verification status (verified | pending | expired | in_remediation), risk level, PEP flag, document checklist, outstanding items and next review date. They key off the retail-banking customer_id (CUST-####); the customer's own profile lives in Purple Retail Banking. Narrow results server-side with this endpoint's named filter params (country, customer_id, status, …) or with OData $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send those as _filter, _orderby, …) instead of paging through everything.","tags":["risk-compliance"]},"post":{"summary":"Create KYC profile","parameters":[{"$ref":"#/components/parameters/risk-compliance.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/risk-compliance.KycProfile"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/risk-compliance.KycProfile"}}}}},"description":"Open a KYC/CDD profile for a customer who does not have one, with initial status, risk level and document checklist.","tags":["risk-compliance"]}},"/purple-suite/risk-compliance/kyc_profiles/{id}":{"get":{"summary":"Get KYC profile","parameters":[{"$ref":"#/components/parameters/risk-compliance.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/risk-compliance.KycProfile"}}}},"404":{"description":"Not found"}},"description":"Fetch one KYC profile by id with its status, risk level, PEP flag, documents, outstanding items and next review date — the check before onboarding, uplifting or remediating a customer.","tags":["risk-compliance"]},"patch":{"summary":"Update KYC profile","parameters":[{"$ref":"#/components/parameters/risk-compliance.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/risk-compliance.KycProfile"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/risk-compliance.KycProfile"}}}},"404":{"description":"Not found"}},"description":"Partially update a KYC profile — record a verified document, change risk level or PEP flag, or reschedule the next review. To drive the remediation workflow itself, use the remediation-case tools.","tags":["risk-compliance"]},"delete":{"summary":"Delete KYC profile","parameters":[{"$ref":"#/components/parameters/risk-compliance.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"id":{"type":"string"}}}}}},"404":{"description":"Not found"}},"description":"Permanently delete a KYC profile. Destructive and irreversible, and it erases regulated CDD evidence — only correct for records created in error.","tags":["risk-compliance"]}},"/purple-suite/risk-compliance/remediation_cases":{"get":{"summary":"List KYC remediation cases","parameters":[{"$ref":"#/components/parameters/risk-compliance.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Max items per page (default 50)"},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Pass the previous response's nextCursor to fetch the next page"},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."},{"in":"query","name":"country","required":false,"schema":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"description":"Filter to a single country (IN | AU | US)"},{"in":"query","name":"customer_id","required":false,"schema":{"type":"string"},"description":"Filter by customer id"},{"in":"query","name":"status","required":false,"schema":{"type":"string","enum":["open","in_progress","pending_customer","completed","closed"]},"description":"Filter by status"},{"in":"query","name":"reason","required":false,"schema":{"type":"string","enum":["expired_doc","re_kyc_nri","sanctions_hit","address_mismatch"]},"description":"Filter by reason"}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/risk-compliance.RemediationCase"}},"nextCursor":{"type":"string","nullable":true},"total":{"type":"integer"}}}}}}},"description":"Remediation cases are the compliance workflow to fix a customer's KYC gap — expired document, NRI re-KYC, sanctions hit or address mismatch — with status open | in_progress | pending_customer | completed | closed, owner, due date and follow-up actions. Compliance remediation, not IT tickets and not fraud investigations. Narrow results server-side with this endpoint's named filter params (country, customer_id, status, …) or with OData $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send those as _filter, _orderby, …) instead of paging through everything.","tags":["risk-compliance"]},"post":{"summary":"Open remediation case","parameters":[{"$ref":"#/components/parameters/risk-compliance.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/risk-compliance.RemediationCase"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/risk-compliance.RemediationCase"}}}}},"description":"Open a KYC remediation case against a customer for a specific reason (e.g. expired_doc, re_kyc_nri). Check for an existing open case first — duplicates chase the customer twice.","tags":["risk-compliance"]}},"/purple-suite/risk-compliance/remediation_cases/{id}":{"get":{"summary":"Get remediation case","parameters":[{"$ref":"#/components/parameters/risk-compliance.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/risk-compliance.RemediationCase"}}}},"404":{"description":"Not found"}},"description":"Fetch one remediation case by id with its reason, status, owner, due date and the follow-up actions already logged.","tags":["risk-compliance"]},"patch":{"summary":"Update remediation case","parameters":[{"$ref":"#/components/parameters/risk-compliance.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/risk-compliance.RemediationCase"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/risk-compliance.RemediationCase"}}}},"404":{"description":"Not found"}},"description":"Partially update remediation case details such as owner, due date or notes. Use the transition tool for status changes and the actions tool to log an outreach step — both enforce their own rules.","tags":["risk-compliance"]},"delete":{"summary":"Delete remediation case","parameters":[{"$ref":"#/components/parameters/risk-compliance.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"id":{"type":"string"}}}}}},"404":{"description":"Not found"}},"description":"Permanently delete a remediation case. Destructive and irreversible, and it loses the regulated remediation trail — transition it to completed or closed instead.","tags":["risk-compliance"]}},"/purple-suite/risk-compliance/remediation_cases/{id}/transition":{"post":{"summary":"Transition a remediation case","description":"Move a KYC remediation case through open → in_progress → pending_customer → completed | closed, recording the reason. Use this rather than patching status: completed and closed are terminal and cannot be transitioned again.","parameters":[{"$ref":"#/components/parameters/risk-compliance.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["open","in_progress","pending_customer","completed","closed"]},"reason":{"type":"string"}},"required":["status"]}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/risk-compliance.RemediationCase"}}}},"400":{"description":"Invalid status"},"404":{"description":"Not found"},"422":{"description":"Invalid state transition"}},"tags":["risk-compliance"]}},"/purple-suite/risk-compliance/remediation_cases/{id}/actions":{"post":{"summary":"Log remediation follow-up action","description":"Append an outreach or follow-up step to a KYC remediation case (e.g. 'Send re-KYC SMS with document upload link'), with an owner and due date. Records what will be done next; it does not change the case status.","parameters":[{"$ref":"#/components/parameters/risk-compliance.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"action":{"type":"string","example":"Send re-KYC SMS with document upload link"},"owner":{"type":"string"},"due":{"type":"string","format":"date"}},"required":["action"]}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/risk-compliance.RemediationCase"}}}},"400":{"description":"Invalid body"},"404":{"description":"Not found"}},"tags":["risk-compliance"]}},"/purple-suite/risk-compliance/fraud_alerts":{"get":{"summary":"List fraud alerts","parameters":[{"$ref":"#/components/parameters/risk-compliance.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Max items per page (default 50)"},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Pass the previous response's nextCursor to fetch the next page"},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."},{"in":"query","name":"country","required":false,"schema":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"description":"Filter to a single country (IN | AU | US)"},{"in":"query","name":"customer_id","required":false,"schema":{"type":"string"},"description":"Filter by customer id"},{"in":"query","name":"indicator","required":false,"schema":{"type":"string","enum":["velocity","geo_anomaly","unauthorized_upi","sim_swap","mule_pattern"]},"description":"Filter by indicator"},{"in":"query","name":"severity","required":false,"schema":{"type":"string","enum":["low","medium","high","critical"]},"description":"Filter by severity"},{"in":"query","name":"status","required":false,"schema":{"type":"string","enum":["new","triaged","dismissed","escalated"]},"description":"Filter by status"}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/risk-compliance.FraudAlert"}},"nextCursor":{"type":"string","nullable":true},"total":{"type":"integer"}}}}}}},"description":"Fraud alerts are machine-generated signals on customer activity (velocity, geo_anomaly, unauthorized_upi, sim_swap, mule_pattern) with severity, score and status new | triaged | dismissed | escalated. They are the raw detections; a confirmed investigation is a fraud case. Narrow results server-side with this endpoint's named filter params (country, customer_id, status, …) or with OData $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send those as _filter, _orderby, …) instead of paging through everything.","tags":["risk-compliance"]},"post":{"summary":"Create fraud alert","parameters":[{"$ref":"#/components/parameters/risk-compliance.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/risk-compliance.FraudAlert"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/risk-compliance.FraudAlert"}}}}},"description":"Raise a fraud alert on a customer's activity with an indicator, severity and a description of the flagged behaviour. Detection normally writes these; a human escalation is usually a fraud case instead.","tags":["risk-compliance"]}},"/purple-suite/risk-compliance/fraud_alerts/{id}":{"get":{"summary":"Get fraud alert","parameters":[{"$ref":"#/components/parameters/risk-compliance.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/risk-compliance.FraudAlert"}}}},"404":{"description":"Not found"}},"description":"Fetch one fraud alert by id with its indicator, severity, score, flagged activity and triage status.","tags":["risk-compliance"]},"patch":{"summary":"Update fraud alert","parameters":[{"$ref":"#/components/parameters/risk-compliance.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/risk-compliance.FraudAlert"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/risk-compliance.FraudAlert"}}}},"404":{"description":"Not found"}},"description":"Partially update fraud alert metadata such as notes or score. Use the triage tool to set triaged/dismissed/escalated — it enforces one-time triage and links the resulting case.","tags":["risk-compliance"]},"delete":{"summary":"Delete fraud alert","parameters":[{"$ref":"#/components/parameters/risk-compliance.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"id":{"type":"string"}}}}}},"404":{"description":"Not found"}},"description":"Permanently delete a fraud alert. Destructive and irreversible — dismiss it via triage instead, so the decision not to act is recorded.","tags":["risk-compliance"]}},"/purple-suite/risk-compliance/fraud_alerts/{id}/triage":{"post":{"summary":"Triage a fraud alert","description":"Decide what happens to a fraud alert: triaged (reviewed, no further action), dismissed (false positive) or escalated — passing case_id links it to the fraud case that will investigate. One-way: an alert that has already been triaged, dismissed or escalated is rejected with 422.","parameters":[{"$ref":"#/components/parameters/risk-compliance.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["triaged","dismissed","escalated"]},"case_id":{"type":"string"}},"required":["status"]}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/risk-compliance.FraudAlert"}}}},"400":{"description":"Invalid status"},"404":{"description":"Not found"},"422":{"description":"Alert already triaged/dismissed/escalated"}},"tags":["risk-compliance"]}},"/purple-suite/risk-compliance/fraud_cases":{"get":{"summary":"List fraud cases","parameters":[{"$ref":"#/components/parameters/risk-compliance.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Max items per page (default 50)"},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Pass the previous response's nextCursor to fetch the next page"},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."},{"in":"query","name":"country","required":false,"schema":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"description":"Filter to a single country (IN | AU | US)"},{"in":"query","name":"customer_id","required":false,"schema":{"type":"string"},"description":"Filter by customer id"},{"in":"query","name":"status","required":false,"schema":{"type":"string","enum":["open","investigating","closed"]},"description":"Filter by status"},{"in":"query","name":"disposition","required":false,"schema":{"type":"string","enum":["confirmed_fraud","false_positive","pending"]},"description":"Filter by disposition"}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/risk-compliance.FraudCase"}},"nextCursor":{"type":"string","nullable":true},"total":{"type":"integer"}}}}}}},"description":"Fraud cases are the human investigations opened off one or more fraud alerts, with an assigned investigator, risk indicators, status open | investigating | closed and a disposition (confirmed_fraud | false_positive | pending). Cases are the investigation; alerts are the raw signals. Narrow results server-side with this endpoint's named filter params (country, customer_id, status, …) or with OData $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send those as _filter, _orderby, …) instead of paging through everything.","tags":["risk-compliance"]},"post":{"summary":"Open fraud case","parameters":[{"$ref":"#/components/parameters/risk-compliance.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/risk-compliance.FraudCase"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/risk-compliance.FraudCase"}}}}},"description":"Open a fraud investigation case for a customer. If it stems from an alert, escalating that alert via the triage tool links the two — prefer that route.","tags":["risk-compliance"]}},"/purple-suite/risk-compliance/fraud_cases/{id}":{"get":{"summary":"Get fraud case","parameters":[{"$ref":"#/components/parameters/risk-compliance.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/risk-compliance.FraudCase"}}}},"404":{"description":"Not found"}},"description":"Fetch one fraud case by id with its status, disposition, assigned investigator, risk indicators and notes.","tags":["risk-compliance"]},"patch":{"summary":"Update fraud case","parameters":[{"$ref":"#/components/parameters/risk-compliance.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/risk-compliance.FraudCase"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/risk-compliance.FraudCase"}}}},"404":{"description":"Not found"}},"description":"Partially update fraud case details such as investigator, risk indicators or notes. Use the transition tool to change status or close with a disposition — closure is enforced there and is irreversible.","tags":["risk-compliance"]},"delete":{"summary":"Delete fraud case","parameters":[{"$ref":"#/components/parameters/risk-compliance.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"id":{"type":"string"}}}}}},"404":{"description":"Not found"}},"description":"Permanently delete a fraud investigation case. Destructive and irreversible, and it destroys the investigation record — close it with a disposition instead.","tags":["risk-compliance"]}},"/purple-suite/risk-compliance/fraud_cases/{id}/transition":{"post":{"summary":"Transition a fraud case","description":"Move a fraud investigation through open → investigating → closed, with an optional note. Closing requires a disposition (confirmed_fraud | false_positive | pending) and is irreversible — a closed case cannot be reopened, so use it only when the investigation is genuinely finished.","parameters":[{"$ref":"#/components/parameters/risk-compliance.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["open","investigating","closed"]},"disposition":{"type":"string","enum":["confirmed_fraud","false_positive","pending"]},"note":{"type":"string"}},"required":["status"]}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/risk-compliance.FraudCase"}}}},"400":{"description":"Invalid status"},"404":{"description":"Not found"},"422":{"description":"Invalid transition / disposition required to close"}},"tags":["risk-compliance"]}},"/purple-suite/risk-compliance/watchlist":{"get":{"summary":"List AML watchlist entries","parameters":[{"$ref":"#/components/parameters/risk-compliance.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Max items per page (default 50)"},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Pass the previous response's nextCursor to fetch the next page"},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."},{"in":"query","name":"country","required":false,"schema":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"description":"Filter to a single country (IN | AU | US)"},{"in":"query","name":"customer_id","required":false,"schema":{"type":"string"},"description":"Filter by customer id"},{"in":"query","name":"list_type","required":false,"schema":{"type":"string","enum":["sanctions","pep","internal_blocklist"]},"description":"Filter by list type"}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/risk-compliance.WatchlistEntry"}},"nextCursor":{"type":"string","nullable":true},"total":{"type":"integer"}}}}}}},"description":"AML screening hits: sanctions, PEP and internal blocklist entries with the matched name, country, list type and match strength, optionally linked to a customer_id. These are screening matches to adjudicate, not blocked cards or frozen accounts. Narrow results server-side with this endpoint's named filter params (country, customer_id, status, …) or with OData $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send those as _filter, _orderby, …) instead of paging through everything.","tags":["risk-compliance"]},"post":{"summary":"Create watchlist entry","parameters":[{"$ref":"#/components/parameters/risk-compliance.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/risk-compliance.WatchlistEntry"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/risk-compliance.WatchlistEntry"}}}}},"description":"Record an AML watchlist hit (sanctions, PEP or internal blocklist) with the matched name and match strength, optionally tied to a customer.","tags":["risk-compliance"]}},"/purple-suite/risk-compliance/watchlist/{id}":{"get":{"summary":"Get watchlist entry","parameters":[{"$ref":"#/components/parameters/risk-compliance.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/risk-compliance.WatchlistEntry"}}}},"404":{"description":"Not found"}},"description":"Fetch one watchlist entry by id with its list type, matched name, country and match strength — the evidence behind a sanctions or PEP flag.","tags":["risk-compliance"]},"patch":{"summary":"Update watchlist entry","parameters":[{"$ref":"#/components/parameters/risk-compliance.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/risk-compliance.WatchlistEntry"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/risk-compliance.WatchlistEntry"}}}},"404":{"description":"Not found"}},"description":"Partially update a watchlist entry — revise the match strength, or attach it to the customer it actually matches after adjudication.","tags":["risk-compliance"]},"delete":{"summary":"Delete watchlist entry","parameters":[{"$ref":"#/components/parameters/risk-compliance.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"id":{"type":"string"}}}}}},"404":{"description":"Not found"}},"description":"Permanently delete a watchlist entry, e.g. after a false-positive adjudication. Destructive and irreversible, and it removes regulated screening evidence.","tags":["risk-compliance"]}},"/purple-suite/risk-compliance/policies":{"get":{"summary":"List compliance policies and SOPs","parameters":[{"$ref":"#/components/parameters/risk-compliance.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Max items per page (default 50)"},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Pass the previous response's nextCursor to fetch the next page"},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."},{"in":"query","name":"country","required":false,"schema":{"type":"string","enum":["IN","AU","US","GB","KR","DE"]},"description":"Filter to a single country (IN | AU | US)"},{"in":"query","name":"policy_type","required":false,"schema":{"type":"string","enum":["SOP","policy","procedure","customer_script"]},"description":"Filter by policy type"},{"in":"query","name":"category","required":false,"schema":{"type":"string","enum":["KYC","payments","lending","branch","fraud"]},"description":"Filter by category"}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/risk-compliance.Policy"}},"nextCursor":{"type":"string","nullable":true},"total":{"type":"integer"}}}}}}},"description":"The compliance knowledge base: policies, procedures, SOPs and customer scripts by category (KYC, payments, lending, branch, fraud) with the owning regulator and effective dates. Use these for 'what does the rule say / what do I tell the customer' questions; IT knowledge articles live in Purple ITSM. Narrow results server-side with this endpoint's named filter params (country, customer_id, status, …) or with OData $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send those as _filter, _orderby, …) instead of paging through everything.","tags":["risk-compliance"]},"post":{"summary":"Create compliance policy","parameters":[{"$ref":"#/components/parameters/risk-compliance.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/risk-compliance.Policy"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/risk-compliance.Policy"}}}}},"description":"Publish a new compliance policy, procedure, SOP or customer script with its category, regulator and effective date.","tags":["risk-compliance"]}},"/purple-suite/risk-compliance/policies/{id}":{"get":{"summary":"Get compliance policy","parameters":[{"$ref":"#/components/parameters/risk-compliance.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/risk-compliance.Policy"}}}},"404":{"description":"Not found"}},"description":"Fetch one policy, SOP or customer script by id with its full text, category, regulator and effective dates — the citation for a compliance answer.","tags":["risk-compliance"]},"patch":{"summary":"Update compliance policy","parameters":[{"$ref":"#/components/parameters/risk-compliance.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/risk-compliance.Policy"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/risk-compliance.Policy"}}}},"404":{"description":"Not found"}},"description":"Update a policy or SOP — revise its text, effective dates or owning regulator when the rule changes.","tags":["risk-compliance"]},"delete":{"summary":"Delete compliance policy","parameters":[{"$ref":"#/components/parameters/risk-compliance.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"id":{"type":"string"}}}}}},"404":{"description":"Not found"}},"description":"Permanently delete a policy or SOP from the compliance knowledge base. Destructive and irreversible — supersede it with an updated version instead.","tags":["risk-compliance"]}},"/purple-suite/scm/suppliers":{"get":{"summary":"List suppliers","description":"Purple SCM suppliers are the vendors the company sources physical goods and services from, with category, country, quality rating, onboarding date and status (active | inactive | pending_approval | blacklisted). Supply-chain vendor master, distinct from ERP finance vendors and from CRM accounts (customers). Filter/sort server-side with $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send these as _filter, _orderby, …) rather than listing everything and filtering locally.","parameters":[{"$ref":"#/components/parameters/scm.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/scm.Supplier"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["scm"]},"post":{"summary":"Onboard supplier","description":"Add a supplier to the Purple SCM vendor master with its category, country and rating. New suppliers usually start pending_approval; search the existing list first to avoid duplicates.","parameters":[{"$ref":"#/components/parameters/scm.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/scm.Supplier"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/scm.Supplier"}}}}},"tags":["scm"]}},"/purple-suite/scm/suppliers/{id}":{"get":{"summary":"Get supplier","description":"Fetch one supplier by id (e.g. SUP-0001) with its status, category, country and rating — the record behind sourcing, contract and quality questions.","parameters":[{"$ref":"#/components/parameters/scm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/scm.Supplier"}}}},"404":{"description":"Not found"}},"tags":["scm"]},"patch":{"summary":"Update supplier","description":"Partially update a supplier — change category, country or rating, or move its status. Setting status to 'blacklisted' stops future sourcing from them and should be deliberate.","parameters":[{"$ref":"#/components/parameters/scm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/scm.Supplier"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/scm.Supplier"}}}},"404":{"description":"Not found"}},"tags":["scm"]},"delete":{"summary":"Delete supplier","description":"Permanently delete a supplier from the vendor master. Destructive and irreversible, and it orphans their contracts, purchase orders, shipments and quality lots — set status to 'inactive' or 'blacklisted' instead.","parameters":[{"$ref":"#/components/parameters/scm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["scm"]}},"/purple-suite/scm/contracts":{"get":{"summary":"List supplier contracts","description":"Purple SCM contracts are the commercial agreements with a supplier (master supply agreements and similar) with value, start/end dates, auto-renew flag and status draft | active | signed | expired | cancelled. Supplier agreements, not customer contracts or purchase orders. Filter/sort server-side with $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send these as _filter, _orderby, …) rather than listing everything and filtering locally.","parameters":[{"$ref":"#/components/parameters/scm.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/scm.Contract"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["scm"]},"post":{"summary":"Create supplier contract","description":"Draft a new commercial agreement with a supplier, with its term dates, value and auto-renew flag. Also the correct move when an expired contract needs replacing, since expired contracts cannot be reactivated.","parameters":[{"$ref":"#/components/parameters/scm.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/scm.Contract"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/scm.Contract"}}}}},"tags":["scm"]}},"/purple-suite/scm/contracts/{id}":{"get":{"summary":"Get supplier contract","description":"Fetch one contract by id (e.g. CON-0001) with its supplier, value, term dates, auto-renew flag and status — the check before raising a PO against it or letting it lapse.","parameters":[{"$ref":"#/components/parameters/scm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/scm.Contract"}}}},"404":{"description":"Not found"}},"tags":["scm"]},"patch":{"summary":"Update supplier contract","description":"Partially update a contract — revise value, dates or auto-renew, or move its status. The state machine blocks two moves (422): an expired contract cannot be returned to active/draft (create a new one), and a cancelled contract cannot be signed.","parameters":[{"$ref":"#/components/parameters/scm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/scm.Contract"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/scm.Contract"}}}},"404":{"description":"Not found"}},"tags":["scm"]},"delete":{"summary":"Delete supplier contract","description":"Permanently delete a contract record. Destructive and irreversible, and it removes the commercial history — set status to 'cancelled' or let it expire instead.","parameters":[{"$ref":"#/components/parameters/scm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["scm"]}},"/purple-suite/scm/shipments":{"get":{"summary":"List inbound shipments","description":"Purple SCM shipments are inbound freight from suppliers, with carrier (FedEx | UPS | DHL | USPS | Maersk | MSC), tracking number, origin, destination, estimated delivery and status pending | in_transit | out_for_delivery | delivered | exception | cancelled. Use for 'where is my delivery' questions. Filter/sort server-side with $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send these as _filter, _orderby, …) rather than listing everything and filtering locally.","parameters":[{"$ref":"#/components/parameters/scm.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/scm.Shipment"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["scm"]},"post":{"summary":"Create shipment","description":"Record an inbound shipment from a supplier with its carrier, tracking number, origin, destination and estimated delivery.","parameters":[{"$ref":"#/components/parameters/scm.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/scm.Shipment"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/scm.Shipment"}}}}},"tags":["scm"]}},"/purple-suite/scm/shipments/{id}":{"get":{"summary":"Get shipment","description":"Fetch one shipment by id (e.g. SHIP-0001) with its carrier, tracking number, route, ETA and current status — the record to quote when tracking a delivery.","parameters":[{"$ref":"#/components/parameters/scm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/scm.Shipment"}}}},"404":{"description":"Not found"}},"tags":["scm"]},"patch":{"summary":"Update shipment","description":"Partially update a shipment — advance its status, correct the ETA or fix carrier/tracking details. Status only moves forward through pending → in_transit → out_for_delivery → delivered (going backwards is rejected with 422), and a delivered shipment cannot be cancelled.","parameters":[{"$ref":"#/components/parameters/scm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/scm.Shipment"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/scm.Shipment"}}}},"404":{"description":"Not found"}},"tags":["scm"]},"delete":{"summary":"Delete shipment","description":"Permanently delete a shipment record. Destructive and irreversible, and it loses the delivery trail — cancel an undelivered shipment via a status update instead.","parameters":[{"$ref":"#/components/parameters/scm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["scm"]}},"/purple-suite/scm/purchase_orders":{"get":{"summary":"List supply-chain purchase orders","description":"Purple SCM purchase orders are procurement POs raised on a supplier for goods (SCMPO-####), with total amount, line-item count, delivery date and status draft | submitted | approved | received | cancelled. This is the supply-chain view; the finance/ERP purchase-order ledger is a separate system with its own ids. Filter/sort server-side with $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send these as _filter, _orderby, …) rather than listing everything and filtering locally.","parameters":[{"$ref":"#/components/parameters/scm.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/scm.PurchaseOrder"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["scm"]},"post":{"summary":"Create purchase order","description":"Raise a purchase order on a supplier with total amount, line items and requested delivery date. New POs start in draft/submitted; approve them by updating status.","parameters":[{"$ref":"#/components/parameters/scm.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/scm.PurchaseOrder"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/scm.PurchaseOrder"}}}}},"tags":["scm"]}},"/purple-suite/scm/purchase_orders/{id}":{"get":{"summary":"Get purchase order","description":"Fetch one supply-chain purchase order by id (e.g. SCMPO-0001) with its supplier, total amount, line items, delivery date and status.","parameters":[{"$ref":"#/components/parameters/scm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/scm.PurchaseOrder"}}}},"404":{"description":"Not found"}},"tags":["scm"]},"patch":{"summary":"Update purchase order","description":"Partially update a purchase order, including approving it (status 'approved') or cancelling it. The state machine rejects (422) cancelling a PO already received or in transit, and approving a cancelled PO.","parameters":[{"$ref":"#/components/parameters/scm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/scm.PurchaseOrder"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/scm.PurchaseOrder"}}}},"404":{"description":"Not found"}},"tags":["scm"]},"delete":{"summary":"Delete purchase order","description":"Permanently delete a purchase order. Destructive and irreversible, and it breaks the audit trail against the supplier — set status to 'cancelled' instead.","parameters":[{"$ref":"#/components/parameters/scm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["scm"]}},"/purple-suite/scm/quality_lots":{"get":{"summary":"List quality inspection lots","description":"Purple SCM quality lots are received batches of goods under incoming inspection, with item SKU, quantity, supplier, defect rate, inspector and status pending | in_inspection | passed | failed | quarantined. Use these for supplier quality and defect-rate questions. Filter/sort server-side with $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send these as _filter, _orderby, …) rather than listing everything and filtering locally.","parameters":[{"$ref":"#/components/parameters/scm.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/scm.QualityLot"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["scm"]},"post":{"summary":"Create quality lot","description":"Register a received batch for incoming quality inspection, with its SKU, quantity and supplier. It starts pending; record the outcome by updating its status and defect rate.","parameters":[{"$ref":"#/components/parameters/scm.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/scm.QualityLot"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/scm.QualityLot"}}}}},"tags":["scm"]}},"/purple-suite/scm/quality_lots/{id}":{"get":{"summary":"Get quality lot","description":"Fetch one quality lot by id (e.g. QL-0001) with its SKU, quantity, supplier, defect rate, inspector and inspection status.","parameters":[{"$ref":"#/components/parameters/scm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/scm.QualityLot"}}}},"404":{"description":"Not found"}},"tags":["scm"]},"patch":{"summary":"Update quality lot","description":"Record an inspection outcome on a lot — set status to in_inspection, passed, failed or quarantined, and write the defect rate and inspector. Quarantining or failing a lot holds the goods, so state the reason.","parameters":[{"$ref":"#/components/parameters/scm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/scm.QualityLot"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/scm.QualityLot"}}}},"404":{"description":"Not found"}},"tags":["scm"]},"delete":{"summary":"Delete quality lot","description":"Permanently delete a quality lot record. Destructive and irreversible, and it erases the supplier's quality evidence — record a failed or quarantined outcome instead.","parameters":[{"$ref":"#/components/parameters/scm.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["scm"]}},"/purple-suite/scm/openapi.json":{"get":{"summary":"Get OpenAPI spec","description":"Returns the OpenAPI 3.0 specification for this API. No authentication required.","security":[],"responses":{"200":{"description":"OpenAPI spec","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["scm"]}},"/purple-suite/tasks-approvals/todos/config":{"get":{"summary":"Get the tenant's Todos tabs and filter facets","description":"Mirrors GET /api/sn_ex_emp_hub_mw/todos/config. No query parameters.","parameters":[{"$ref":"#/components/parameters/tasks-approvals.InstanceId"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/tasks-approvals.TodosConfigResult"}}}}},"tags":["tasks-approvals"]}},"/purple-suite/tasks-approvals/todos":{"get":{"summary":"List the caller's todos (tasks + approvals, unified)","description":"Mirrors GET /api/sn_ex_emp_hub_mw/todos?tabs=<tab>. The effective caller resolves via X-PS-Impersonate-User / the instance's default MCP user.","parameters":[{"$ref":"#/components/parameters/tasks-approvals.InstanceId"},{"in":"query","name":"tabs","schema":{"type":"string","enum":["open","completed"],"default":"open"},"description":"Which tab to list."},{"in":"query","name":"filters","schema":{"type":"string"},"description":"One or more filterId values from /todos/config (comma-separated or repeated). ANDed across categories, ORed within a category."},{"in":"query","name":"limit","schema":{"type":"integer"},"description":"Max todos to return."},{"in":"query","name":"excludeSysIds","schema":{"type":"string"},"description":"Comma-separated ids to exclude (already seen in a prior page/turn)."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/tasks-approvals.TodosResult"}}}},"400":{"description":"Cannot resolve the effective caller"}},"tags":["tasks-approvals"]}},"/purple-suite/tasks-approvals/tasks":{"get":{"summary":"List tasks","parameters":[{"$ref":"#/components/parameters/tasks-approvals.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/tasks-approvals.Task"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["tasks-approvals"]},"post":{"summary":"Create task","parameters":[{"$ref":"#/components/parameters/tasks-approvals.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/tasks-approvals.Task"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/tasks-approvals.Task"}}}}},"tags":["tasks-approvals"]}},"/purple-suite/tasks-approvals/tasks/{id}":{"get":{"summary":"Get task","parameters":[{"$ref":"#/components/parameters/tasks-approvals.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/tasks-approvals.Task"}}}},"404":{"description":"Not found"}},"tags":["tasks-approvals"]},"patch":{"summary":"Update task","parameters":[{"$ref":"#/components/parameters/tasks-approvals.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/tasks-approvals.Task"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/tasks-approvals.Task"}}}},"404":{"description":"Not found"}},"tags":["tasks-approvals"]},"delete":{"summary":"Delete task","parameters":[{"$ref":"#/components/parameters/tasks-approvals.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["tasks-approvals"]}},"/purple-suite/tasks-approvals/approvals":{"get":{"summary":"List approvals","parameters":[{"$ref":"#/components/parameters/tasks-approvals.InstanceId"},{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."},{"in":"query","name":"$filter","schema":{"type":"string"},"description":"OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."},{"in":"query","name":"$select","schema":{"type":"string"},"description":"Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."},{"in":"query","name":"$orderby","schema":{"type":"string"},"description":"Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."},{"in":"query","name":"$search","schema":{"type":"string"},"description":"Free-text, case-insensitive substring match across the resource's searchable fields."},{"in":"query","name":"$top","schema":{"type":"integer"},"description":"Maximum number of items to return (page size)."},{"in":"query","name":"$skip","schema":{"type":"integer"},"description":"Number of matching items to skip before returning results (offset paging). Combine with $top."},{"in":"query","name":"$count","schema":{"type":"boolean"},"description":"When true, include @odata.count (total matches after $filter/$search, before paging) in the response."}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/tasks-approvals.Approval"}},"nextCursor":{"type":"string","nullable":true,"description":"Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."},"total":{"type":"integer","description":"Total documents in the collection."},"@odata.count":{"type":"integer","description":"Present only when $count=true: total matches after $filter/$search."},"@odata.truncated":{"type":"boolean","description":"Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}},"tags":["tasks-approvals"]},"post":{"summary":"Create approval","parameters":[{"$ref":"#/components/parameters/tasks-approvals.InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/tasks-approvals.Approval"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/tasks-approvals.Approval"}}}}},"tags":["tasks-approvals"]}},"/purple-suite/tasks-approvals/approvals/{id}":{"get":{"summary":"Get approval","parameters":[{"$ref":"#/components/parameters/tasks-approvals.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/tasks-approvals.Approval"}}}},"404":{"description":"Not found"}},"tags":["tasks-approvals"]},"patch":{"summary":"Update approval","parameters":[{"$ref":"#/components/parameters/tasks-approvals.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/tasks-approvals.Approval"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/tasks-approvals.Approval"}}}},"404":{"description":"Not found"}},"tags":["tasks-approvals"]},"delete":{"summary":"Delete approval","parameters":[{"$ref":"#/components/parameters/tasks-approvals.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"Not found"}},"tags":["tasks-approvals"]}},"/purple-suite/tasks-approvals/approvals/{id}/approve":{"post":{"summary":"Approve an approval","parameters":[{"$ref":"#/components/parameters/tasks-approvals.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["decided_by"],"properties":{"decided_by":{"type":"string","example":"abraham.lincoln@acme.com"},"reason":{"type":"string"}}}}}},"responses":{"200":{"description":"Updated approval","content":{"application/json":{"schema":{"$ref":"#/components/schemas/tasks-approvals.Approval"}}}},"404":{"description":"Not found"},"422":{"description":"Already decided"}},"tags":["tasks-approvals"]}},"/purple-suite/tasks-approvals/approvals/{id}/reject":{"post":{"summary":"Reject an approval","parameters":[{"$ref":"#/components/parameters/tasks-approvals.InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["decided_by","reason"],"properties":{"decided_by":{"type":"string"},"reason":{"type":"string"}}}}}},"responses":{"200":{"description":"Updated approval","content":{"application/json":{"schema":{"$ref":"#/components/schemas/tasks-approvals.Approval"}}}},"404":{"description":"Not found"},"422":{"description":"Already decided"}},"tags":["tasks-approvals"]}}},"x-purple-suite-apps":[{"id":"ats","title":"Purple Suite ATS API","pathPrefix":"/purple-suite/ats","specUrl":"https://marketplace.moveworks.com/api/purple-suite/ats/openapi.json"},{"id":"bank-ops","title":"Purple Bank Ops API","pathPrefix":"/purple-suite/bank-ops","specUrl":"https://marketplace.moveworks.com/api/purple-suite/bank-ops/openapi.json"},{"id":"calendar","title":"Purple Suite Calendar API","pathPrefix":"/purple-suite/calendar","specUrl":"https://marketplace.moveworks.com/api/purple-suite/calendar/openapi.json"},{"id":"chat","title":"Purple Suite Chat API","pathPrefix":"/purple-suite/chat","specUrl":"https://marketplace.moveworks.com/api/purple-suite/chat/openapi.json"},{"id":"community","title":"Purple Suite Community API","pathPrefix":"/purple-suite/community","specUrl":"https://marketplace.moveworks.com/api/purple-suite/community/openapi.json"},{"id":"crm","title":"Purple Suite CRM API","pathPrefix":"/purple-suite/crm","specUrl":"https://marketplace.moveworks.com/api/purple-suite/crm/openapi.json"},{"id":"dex","title":"Purple Suite DEX API","pathPrefix":"/purple-suite/dex","specUrl":"https://marketplace.moveworks.com/api/purple-suite/dex/openapi.json"},{"id":"erp","title":"Purple Suite ERP API","pathPrefix":"/purple-suite/erp","specUrl":"https://marketplace.moveworks.com/api/purple-suite/erp/openapi.json"},{"id":"expense","title":"Purple Expense API","pathPrefix":"/purple-suite/expense","specUrl":"https://marketplace.moveworks.com/api/purple-suite/expense/openapi.json"},{"id":"grid","title":"Purple Suite Grid API","pathPrefix":"/purple-suite/grid","specUrl":"https://marketplace.moveworks.com/api/purple-suite/grid/openapi.json"},{"id":"hrbp","title":"Purple Suite HRBP API","pathPrefix":"/purple-suite/hrbp","specUrl":"https://marketplace.moveworks.com/api/purple-suite/hrbp/openapi.json"},{"id":"hris","title":"Purple Suite HRIS API","pathPrefix":"/purple-suite/hris","specUrl":"https://marketplace.moveworks.com/api/purple-suite/hris/openapi.json"},{"id":"identity","title":"Purple Identity API","pathPrefix":"/purple-suite/identity","specUrl":"https://marketplace.moveworks.com/api/purple-suite/identity/openapi.json"},{"id":"itsm","title":"Purple Suite ITSM API","pathPrefix":"/purple-suite/itsm","specUrl":"https://marketplace.moveworks.com/api/purple-suite/itsm/openapi.json"},{"id":"lms","title":"Purple Suite LMS API","pathPrefix":"/purple-suite/lms","specUrl":"https://marketplace.moveworks.com/api/purple-suite/lms/openapi.json"},{"id":"manager-experience","title":"Purple Suite Manager Experience API","pathPrefix":"/purple-suite/manager-experience","specUrl":"https://marketplace.moveworks.com/api/purple-suite/manager-experience/openapi.json"},{"id":"pages","title":"Purple Suite Pages API","pathPrefix":"/purple-suite/pages","specUrl":"https://marketplace.moveworks.com/api/purple-suite/pages/openapi.json"},{"id":"pm","title":"Purple Suite PM API","pathPrefix":"/purple-suite/pm","specUrl":"https://marketplace.moveworks.com/api/purple-suite/pm/openapi.json"},{"id":"purple-talent","title":"Purple Suite PurpleTalent API","pathPrefix":"/purple-suite/purple-talent","specUrl":"https://marketplace.moveworks.com/api/purple-suite/purple-talent/openapi.json"},{"id":"retail-banking","title":"Purple Retail Banking API","pathPrefix":"/purple-suite/retail-banking","specUrl":"https://marketplace.moveworks.com/api/purple-suite/retail-banking/openapi.json"},{"id":"risk-compliance","title":"Purple Risk & Compliance API","pathPrefix":"/purple-suite/risk-compliance","specUrl":"https://marketplace.moveworks.com/api/purple-suite/risk-compliance/openapi.json"},{"id":"scm","title":"Purple Suite SCM API","pathPrefix":"/purple-suite/scm","specUrl":"https://marketplace.moveworks.com/api/purple-suite/scm/openapi.json"},{"id":"tasks-approvals","title":"Purple Suite Tasks & Approvals API","pathPrefix":"/purple-suite/tasks-approvals","specUrl":"https://marketplace.moveworks.com/api/purple-suite/tasks-approvals/openapi.json"}]}