{"openapi":"3.0.0","info":{"title":"Purple Suite HRIS API","version":"1.0.0","description":"\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/purple-suite/hris"}],"security":[{"patAuth":[]}],"components":{"securitySchemes":{"patAuth":{"type":"http","scheme":"bearer"}},"parameters":{"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":{"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"]},"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"]},"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}}},"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}}},"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}}},"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}}},"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"]},"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}}},"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"]},"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"]},"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"]},"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"]}}},"paths":{"/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/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"}}}},"/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/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"}}}},"/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/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/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"}}}}}}}},"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/InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Employee"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Employee"}}}},"400":{"description":"Missing required field or invalid status"},"409":{"description":"Employee with this email already exists"}}}},"/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/InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Employee"}}}},"404":{"description":"Not found"}}},"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/InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Employee"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Employee"}}}},"404":{"description":"Not found"},"422":{"description":"State machine violation (e.g. reactivating a terminated employee)"}}},"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/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"}}}},"/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/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/Employee"}}}},"400":{"description":"Invalid status value"},"404":{"description":"Employee not found"},"422":{"description":"Transition not allowed by state machine rules"}}}},"/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/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/Employee"}}}},"404":{"description":"Employee not found"}}}},"/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/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/Employee/properties/skills/items"}}}}}}},"404":{"description":"Employee not found"}}}},"/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/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/Employee/properties/reviews/items"}}}}}}},"404":{"description":"Employee not found"}}}},"/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/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/Employee/properties/goals/items"}}}}}}},"404":{"description":"Employee not found"}}}},"/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/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/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"}}}}}}}},"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/InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Department"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Department"}}}}}}},"/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/InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Department"}}}},"404":{"description":"Not found"}}},"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/InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Department"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Department"}}}},"404":{"description":"Not found"}}},"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/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"}}}},"/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/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/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"}}}}}}}},"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/InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PayRun"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PayRun"}}}}}}},"/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/InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PayRun"}}}},"404":{"description":"Not found"}}},"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/InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PayRun"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PayRun"}}}},"404":{"description":"Not found"},"422":{"description":"State machine violation (e.g. modifying a completed run, or skipping processing status)"}}},"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/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"}}}},"/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/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/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"}}}}}}}},"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/InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Timesheet"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/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"}}}},"/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/InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Timesheet"}}}},"404":{"description":"Not found"}}},"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/InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Timesheet"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Timesheet"}}}},"404":{"description":"Not found"}}},"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/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"}}}},"/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/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/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"}}}}}}}},"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/InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Benefit"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/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"}}}},"/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/InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Benefit"}}}},"404":{"description":"Not found"}}},"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/InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Benefit"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Benefit"}}}},"404":{"description":"Not found"}}},"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/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"}}}},"/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/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/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"}}}}}}}},"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/InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReviewCycle"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReviewCycle"}}}}}}},"/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/InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReviewCycle"}}}},"404":{"description":"Not found"}}},"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/InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReviewCycle"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReviewCycle"}}}},"404":{"description":"Not found"}}},"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/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"}}}},"/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/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/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"}}}}}}}},"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/InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TimeOffRequest"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TimeOffRequest"}}}},"400":{"description":"Missing required field or invalid type/status"},"404":{"description":"Employee not found"}}}},"/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/InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TimeOffRequest"}}}},"404":{"description":"Not found"}}},"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/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/TimeOffRequest"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TimeOffRequest"}}}},"404":{"description":"Not found"}}}},"/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/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/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"}}}}}}}},"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/InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkAuthorization"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkAuthorization"}}}},"400":{"description":"Validation error"}}}},"/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/InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkAuthorization"}}}},"404":{"description":"Not found"}}},"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/InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkAuthorization"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkAuthorization"}}}},"404":{"description":"Not found"}}},"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/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"}}}},"/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/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/WorkAuthorization"}}}},"404":{"description":"Authorization not found"},"422":{"description":"Authorization cannot be renewed (no expiry, or already filed)"}}}},"/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/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/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"}}}}}}}},"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/InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobFamily"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobFamily"}}}},"400":{"description":"Validation / missing reference"},"409":{"description":"Duplicate"}}}},"/job-families/{id}":{"get":{"summary":"Get a job family by id","description":"Returns one career grouping's name and description.","parameters":[{"$ref":"#/components/parameters/InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobFamily"}}}},"404":{"description":"Not found"}}},"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/InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobFamily"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobFamily"}}}},"400":{"description":"Validation / missing reference"},"404":{"description":"Not found"}}},"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/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"}}}},"/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/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/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"}}}}}}}},"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/InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobLevel"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobLevel"}}}},"400":{"description":"Validation / missing reference"},"409":{"description":"Duplicate"}}}},"/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/InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobLevel"}}}},"404":{"description":"Not found"}}},"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/InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobLevel"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobLevel"}}}},"400":{"description":"Validation / missing reference"},"404":{"description":"Not found"}}},"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/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"}}}},"/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/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/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"}}}}}}}},"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/InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobProfile"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobProfile"}}}},"400":{"description":"Validation / missing reference"},"409":{"description":"Duplicate"}}}},"/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/InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobProfile"}}}},"404":{"description":"Not found"}}},"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/InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobProfile"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobProfile"}}}},"400":{"description":"Validation / missing reference"},"404":{"description":"Not found"}}},"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/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"}}}},"/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/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/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"}}}}}}}},"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/InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobRole"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobRole"}}}},"400":{"description":"Validation / missing reference"},"409":{"description":"Duplicate"}}}},"/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/InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobRole"}}}},"404":{"description":"Not found"}}},"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/InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Fields to update (partial)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobRole"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobRole"}}}},"400":{"description":"Validation / missing reference"},"404":{"description":"Not found"}}},"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/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"}}}}}}