{"serverInfo":{"name":"nifty-mcp","version":"0.9.1"},"authentication":{"required":true,"schemes":["bearer"]},"tools":[{"name":"knowledge_query","description":"Read (list / get / search) knowledge resources: annotation (A drawing or comment-anchor placed on a file); document (A project-scoped rich-text document (Nifty native doc)); file (An uploaded file attached to a project, task, document, message, or folder). Pass {resource, operation} plus the operation's args. Read verb ops: content (Read a document body as Markdown — the rich-text content, which list and get do not return). For project-scoped resources here (document): \"search\" REQUIRES `projectId` (no workspace-wide fallback — omitting it errors `project_not_specified`); \"list\" does NOT require it (omitting it falls back to a workspace-wide scan).","inputSchema":{"type":"object","properties":{"resource":{"type":"string","enum":["annotation","document","file"],"description":"Which resource within this domain group to act on."},"operation":{"type":"string","enum":["content","get","list","search"],"description":"Which operation to run on the resource. Valid arguments depend on the (resource, operation) pair — unknown arguments are rejected with an error that names the accepted ones (the top-level schema is permissive because it forwards to a strict per-operation validator)."},"createdAt":{"type":"string","description":"Creation timestamp."},"createdAt_gt":{"type":"string","description":"Filter \"createdAt\" by gt (operator filter)."},"createdAt_gte":{"type":"string","description":"Filter \"createdAt\" by gte (operator filter)."},"createdAt_lt":{"type":"string","description":"Filter \"createdAt\" by lt (operator filter)."},"createdAt_lte":{"type":"string","description":"Filter \"createdAt\" by lte (operator filter)."},"updatedAt":{"type":"string","description":"Last update timestamp."},"updatedAt_gt":{"type":"string","description":"Filter \"updatedAt\" by gt (operator filter)."},"updatedAt_gte":{"type":"string","description":"Filter \"updatedAt\" by gte (operator filter)."},"updatedAt_lt":{"type":"string","description":"Filter \"updatedAt\" by lt (operator filter)."},"updatedAt_lte":{"type":"string","description":"Filter \"updatedAt\" by lte (operator filter)."},"type":{"type":"string","description":"type — varies by resource; validated per (resource, operation) at call time"},"type_in":{"type":"array","items":{"type":"string"},"description":"Filter \"type\" by in (operator filter)."},"videoTimeSeconds":{"type":"number","description":"Playback position in seconds for a video annotation. Null for image annotations."},"videoTimeSeconds_gt":{"type":"number","description":"Filter \"videoTimeSeconds\" by gt (operator filter)."},"videoTimeSeconds_gte":{"type":"number","description":"Filter \"videoTimeSeconds\" by gte (operator filter)."},"videoTimeSeconds_lt":{"type":"number","description":"Filter \"videoTimeSeconds\" by lt (operator filter)."},"videoTimeSeconds_lte":{"type":"number","description":"Filter \"videoTimeSeconds\" by lte (operator filter)."},"videoTimeSeconds_null":{"type":"number","description":"Filter \"videoTimeSeconds\" by null (operator filter)."},"videoTimeSeconds_notNull":{"type":"number","description":"Filter \"videoTimeSeconds\" by notNull (operator filter)."},"fileId":{"type":"string","description":"The file this annotation is drawn on. Required and immutable."},"fileId_in":{"type":"array","items":{"type":"string"},"description":"Filter \"fileId\" by in (operator filter)."},"taskId":{"type":"string","description":"The task that holds this annotation's comment thread. Created automatically when someone comments on the annotation in the Nifty app; null until then."},"taskId_in":{"type":"array","items":{"type":"string"},"description":"Filter \"taskId\" by in (operator filter)."},"purgeAt":{"type":"string","description":"When this resource will be permanently deleted — soft-delete time + 30-day retention; null while live."},"purgeAt_null":{"type":"string","description":"Filter \"purgeAt\" by null (operator filter)."},"purgeAt_notNull":{"type":"string","description":"Filter \"purgeAt\" by notNull (operator filter)."},"purgeAt_gt":{"type":"string","description":"Filter \"purgeAt\" by gt (operator filter)."},"purgeAt_gte":{"type":"string","description":"Filter \"purgeAt\" by gte (operator filter)."},"purgeAt_lt":{"type":"string","description":"Filter \"purgeAt\" by lt (operator filter)."},"purgeAt_lte":{"type":"string","description":"Filter \"purgeAt\" by lte (operator filter)."},"name":{"type":"string","description":"Document display name. Required on create; editable."},"name_contains":{"type":"string","description":"Filter \"name\" by contains (operator filter)."},"name_startsWith":{"type":"string","description":"Filter \"name\" by startsWith (operator filter)."},"editorVersion":{"type":"string","description":"Editor generation: `1` = legacy, `2` = current (Plate). Read-only; server-forces `2` on v3 create. A `1` document's body is NOT writable here (not a Yjs doc) — `append`/`editText` refuse it; only `name`, `description`, `locked`, `archived` are editable. Legacy `1` docs are numerous and permanent, so filter `editorVersion=2` when listing documents to work on."},"editorVersion_in":{"type":"array","items":{"type":"string"},"description":"Filter \"editorVersion\" by in (operator filter)."},"accessType":{"type":"string","enum":["public","limited"],"description":"Who can see this document. `public` (the default) — everyone in the project. `limited` — only the document's own member list, which starts out as JUST THE RESOLVED AUTHOR: when an agent or integration creates a limited document, the author it resolves to is the one who can reach it, not the caller. `private` — a legacy state the API cannot set; those documents are readable and filterable but cannot be created here. Choose this when you create the document — it cannot be changed afterwards through the API (change it in the Nifty app instead). A child document always inherits its parent's access type."},"accessType_in":{"type":"array","items":{"type":"string"},"description":"Filter \"accessType\" by in (operator filter)."},"archived":{"type":"boolean","description":"Whether the document is archived. Archived documents stay in the project but are hidden from default views. Independent of deletion — an archived document is not deleted, and a deleted document is not archived."},"projectId":{"type":"string","pattern":"^[0-9A-Za-z_!]{1,16}$","description":"Scope \"list\"/\"search\" to one project (project-scoped resources only). REQUIRED for \"search\": unlike \"list\", search has NO workspace-wide fallback — omitting it on \"search\" errors `project_not_specified`. \"list\" does NOT require it: omitting it on \"list\" falls back to a workspace-wide scan across every project you can access."},"projectId_in":{"type":"array","items":{"type":"string"},"description":"Filter \"projectId\" by in (operator filter)."},"parentDocumentId":{"type":"string","description":"The document this one is nested under — a parent in the same project. Set once at creation and immutable thereafter."},"parentDocumentId_in":{"type":"array","items":{"type":"string"},"description":"Filter \"parentDocumentId\" by in (operator filter)."},"parentDocumentId_null":{"type":"string","description":"Filter \"parentDocumentId\" by null (operator filter)."},"parentDocumentId_notNull":{"type":"string","description":"Filter \"parentDocumentId\" by notNull (operator filter)."},"processed":{"type":"string","description":"False until server-side post-processing (thumbnail generation, virus scanning) finishes — `thumbnail` and `download` may be null until then. Typically a few seconds."},"uploaderId":{"type":"string","description":"The team member who uploaded the file — a member id, so resolve it with `?expand=uploader` or `GET /members/{id}`. Null when uploaded by an agent or an integration — see `createdByActorType`."},"uploaderId_in":{"type":"array","items":{"type":"string"},"description":"Filter \"uploaderId\" by in (operator filter)."},"documentId":{"type":"string","description":"Attach the file to this document. At least one of `projectId`, `taskId`, `documentId`, `messageId` or `folderId` is required on create. Fixed at upload."},"documentId_in":{"type":"array","items":{"type":"string"},"description":"Filter \"documentId\" by in (operator filter)."},"messageId":{"type":"string","description":"Attach the file to this message. At least one of `projectId`, `taskId`, `documentId`, `messageId` or `folderId` is required on create. Fixed at upload."},"messageId_in":{"type":"array","items":{"type":"string"},"description":"Filter \"messageId\" by in (operator filter)."},"folderId":{"type":"string","description":"Place the file in this folder. At least one of `projectId`, `taskId`, `documentId`, `messageId` or `folderId` is required on create. A file's project / task / document / message parent is fixed at upload; only its folder can be changed afterwards."},"folderId_in":{"type":"array","items":{"type":"string"},"description":"Filter \"folderId\" by in (operator filter)."},"annotationTaskId":{"type":"string","description":"The task that holds this file's annotation thread, created when the file is opened in annotation mode. Read-only."},"annotationTaskId_in":{"type":"array","items":{"type":"string"},"description":"Filter \"annotationTaskId\" by in (operator filter)."},"id":{"type":"string","pattern":"^[0-9A-Za-z_!]{1,16}$","description":"Entity id — required for \"get\" and for instance-target read verb ops."},"limit":{"type":"integer","exclusiveMinimum":0,"description":"Items per page for \"list\"/\"search\" (default 50). Over the server cap it is silently clamped, not rejected — a short page does NOT mean the end; page until `nextCursor` is null."},"cursor":{"type":"string","description":"Opaque pagination cursor returned by a previous \"list\"/\"search\"."},"includeTotal":{"type":"boolean","description":"Opt-in. List `total` is `null` by default for performance (an extra COUNT over the full set). Set true to get `total` = the count of the FULL filtered + ACL-scoped result set (NOT the current page; ignores limit/cursor). The `total` key is always present: integer when requested, null when not. To ask only 'how many', pass includeTotal:true with limit:1 so you don't over-fetch rows. List-only (search always returns total)."},"query":{"type":"string","description":"Full-text query — REQUIRED for operation \"search\". The \"list\" operation has NO free-text search: it filters only by the resource's filterable fields. To search by text, use operation:\"search\" with \"query\" (not a \"search\" key on \"list\")."},"expand":{"type":"string","description":"Comma-separated relations to expand inline on the returned entity/entities. Nested paths allowed with dots (e.g. members.member) up to depth 4; nesting into a relation also requires that target's read scope. Available per resource — annotation: file/task/messages/createdByActor/updatedByActor; document: project/author/parentDocument/deletedBy/createdByActor/updatedByActor/labels; file: uploader/deletedBy/createdByActor/updatedByActor/project/task/document/message/annotationTask/subscribers/labels/annotations. Unknown relations rejected."},"sort":{"type":"string","description":"Sort \"list\"/\"search\" results by ONE column. Descending: prefix \"-\" (e.g. \"-updatedAt\") OR suffix \":desc\" (e.g. \"updatedAt:desc\"). Ascending: bare column (e.g. \"updatedAt\") OR suffix \":asc\". A single string — NOT separate sortBy/sortDir, and do NOT combine \"-\" with \":asc\"/\":desc\". An unlisted column is rejected with an error naming the valid ones. Sortable columns + default per resource — annotation: createdAt/updatedAt (default createdAt); document: name/createdAt (default createdAt); file: name/size/createdAt/updatedAt/projectId/taskId/folderId/messageId (default -createdAt). Those defaults are LIST-only: omit sort on \"list\" to get the default shown, but omit it on \"search\" and results come back ranked by text relevance instead. Pass sort explicitly (e.g. sort=order) to force a specific order on either."}},"required":["resource","operation"],"additionalProperties":true,"$schema":"http://json-schema.org/draft-07/schema#"},"annotations":{"title":"Find docs and files","readOnlyHint":true,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false},"execution":{"taskSupport":"forbidden"},"_meta":{"requiredScopes":["annotations:read","documents:read","files:read"]}},{"name":"knowledge_mutate","description":"Create or update knowledge resources: annotation (A drawing or comment-anchor placed on a file); document (A project-scoped rich-text document (Nifty native doc)); file (An uploaded file attached to a project, task, document, message, or folder). Pass {resource, operation} plus the resource's writable fields. Verb ops beyond create/update: append (Append Markdown to the END of a document's body); editText (Replace exact text within a document's body — the way to EDIT existing content, as opposed to `append` (which only adds to the end) or `update` (which cannot touch the body at all)); createUploadUrl (Mint a presigned S3 upload URL — step 1 of uploading a new file); createFromContent (Create a file from inline base64 content in ONE call — for clients that cannot HTTP PUT raw bytes to S3 (sandboxed/egress-blocked environments); max 3 MB decoded); uploadFromUrl (Create a file from a public http/https URL in ONE call — the SERVER fetches it, for a client that has the address but cannot fetch the bytes (egress-blocked/sandboxed environments); the URL goes in `sourceUrl` (not `uploadKey`, which on file create is the pointer to already-uploaded bytes, and not `url`, which is the download link on a stored file), max 25 MB, and at least one parent is required (taskId, projectId, documentId, messageId or folderId)). Also links/unlinks relations via addLabels, addSubscribers, removeLabels, removeSubscribers — pass {resource, operation, id, ids}. Batch writes: bulkCreate, bulkUpdate — pass {resource, operation, items:[…], onError?}. Recovering a bulkCreate depends on whether you got a result envelope back. GOT ONE: trust it — it is index-aligned, rows marked ok are created, and you may send a NEW batch containing only the rows that never got created. NO ENVELOPE (timeout / lost response): resend the SAME items in the SAME order — that exact batch is deduplicated per row and replays what already landed instead of re-creating it. Do NOT re-split it or drop items first; that changes the dedup key and DOES create duplicates. bulkUpdate carries no dedup key, so it is safe to resend only because each item is an absolute field set — never resend one whose values are relative to the current value. Concurrency-safe updates: on a single-row update, pass expectedUpdatedAt = the updatedAt from the read your edit is based on; a stale_write (409) means the row changed since — re-read it, re-derive your change, and retry with the fresh updatedAt (never a timestamp echoed by the error). The guard requires at least one field change. Bulk items do not accept it: with onError absent, one item carrying it rejects the WHOLE batch client-side (0 HTTP calls); onError:'continue' ships the valid rows and returns per-item errors. Clearing a value: a field whose type includes \"null\" accepts an explicit JSON null to clear it (for an FK, to unlink it). Omitting a key leaves that field UNCHANGED — omission and null are different requests, so send only the keys you mean to change. Create a document to move long-form content into a collaborative space — pass `content` (Markdown) to author the body up front, and set `description` (1–3 sentences) so agents can tell what each document is for straight from list results — read descriptions before fetching bodies. After creation, `append` extends the body, `editText` replaces exact text in it, the `content` query operation reads it; `update` changes only the shell (name/description/locked/archived). PREFER attaching an existing file or document over uploading a duplicate. Uploading a file: bytes in hand and ≤3 MB decoded, or an environment that cannot make raw HTTP PUTs (sandboxed clients) — use `createFromContent`: ONE call, bytes in `contentBase64` (NOT `content`, the DOCUMENT body on this tool), `name` WITH its extension, at least one parent FK (taskId/projectId/documentId/messageId/folderId; taskId alone derives the project). Bigger files with open egress: `createUploadUrl` → HTTP PUT the bytes to `uploadUrl` sending exactly the returned headers → file `create` with the returned `url`, ONLY after that PUT succeeded (`create` records metadata without verifying the bytes). Only a public URL to the asset, never the bytes themselves: `uploadFromUrl` — the SERVER fetches the address you pass as `sourceUrl` (NOT `url`, which on file `create` is the S3 pointer to bytes that are already uploaded); max 25 MB, up to 3 redirects, private/internal addresses refused, same parent-FK rule (taskId alone derives the project). This tool's `name`/`projectId` param descriptions describe documents, not files. To put an existing document ON a task, use the tasks tool's `attachDocument`.","inputSchema":{"type":"object","properties":{"resource":{"type":"string","enum":["annotation","document","file"],"description":"Which resource within this domain group to act on."},"operation":{"type":"string","enum":["addLabels","addSubscribers","append","bulkCreate","bulkUpdate","create","createFromContent","createUploadUrl","editText","removeLabels","removeSubscribers","update","uploadFromUrl"],"description":"Which operation to run on the resource. Valid arguments depend on the (resource, operation) pair — unknown arguments are rejected with an error that names the accepted ones (the top-level schema is permissive because it forwards to a strict per-operation validator)."},"id":{"type":"string","pattern":"^[0-9A-Za-z_!]{1,16}$","description":"Entity id — for \"update\", the add{Relation}/remove{Relation} ops, and instance-target verb ops (id = the entity acted on)."},"ids":{"type":"array","items":{"type":"string","pattern":"^[0-9A-Za-z_!]{1,16}$","description":"related entity id"},"description":"Target ids for an add{Relation}/remove{Relation} op (1..500; auto-chunked)."},"items":{"type":"array","items":{"type":"object","properties":{},"additionalProperties":true},"minItems":1,"description":"bulkCreate/bulkUpdate only: array of per-item bodies, each the same shape as a single create/update (bulkUpdate items include id). Max 150."},"onError":{"type":"string","enum":["abort","continue"],"description":"bulk ops only: abort (default, stop at first failure) or continue (best-effort, per-item errors in the result)."},"expectedUpdatedAt":{"type":"string","format":"date-time","description":"update (single-row) only: concurrency guard — the `updatedAt` from the read your edit is based on. 409 stale_write if the row changed since; re-read, re-derive, retry with the fresh value. Requires ≥1 field change. Not accepted on bulk items."},"type":{"type":"string","enum":["image","video"],"description":"Annotation kind: image or video."},"geometry":{"type":"object","additionalProperties":{},"description":"Where the annotation sits on an image: the shape kind (`POINT`, `RECTANGLE` or `OVAL`) plus `x` / `y` / `width` / `height`. All four are PERCENTAGES of the file's own dimensions (0–100), not pixels, so an annotation keeps its place at any display size. A `POINT` needs only `x` and `y`. Empty for a bare anchor."},"videoTimeSeconds":{"type":["number","null"],"description":"Playback position in seconds for a video annotation. Null for image annotations."},"fileId":{"type":"string","description":"The file this annotation is drawn on. Required and immutable."},"name":{"type":"string","description":"Document display name. Required on create; editable."},"description":{"type":["string","null"],"description":"1–3 sentence summary of what this document is for (max 2,000 chars; null = none). Surfaced in list results so agents can discover a project's knowledge without fetching bodies. Set it when creating a document; keep it short — it is metadata, not a second body. Supports Markdown. Embed refs: `<@memberId>` person, `<?taskId>` task, `<#projectId>` project, `<??taskGroupId>` status, `<^milestoneId>` list, `<~labelId>` label, `<$subteamId>` portfolio, `<!discussionId>` discussion. A project niceId + number (e.g. `PROJ-123`) auto-links that task; `:shortcode:` renders an emoji. Write mention/ref glyphs as plain literal text — wrapping one in backticks or other code formatting renders it as inert code instead of a resolved mention/link."},"accessType":{"type":"string","enum":["public","limited"],"description":"Who can see this document. `public` (the default) — everyone in the project. `limited` — only the document's own member list, which starts out as JUST THE RESOLVED AUTHOR: when an agent or integration creates a limited document, the author it resolves to is the one who can reach it, not the caller. `private` — a legacy state the API cannot set; those documents are readable and filterable but cannot be created here. Choose this when you create the document — it cannot be changed afterwards through the API (change it in the Nifty app instead). A child document always inherits its parent's access type."},"locked":{"type":"boolean","description":"Whether the document is locked against edits."},"archived":{"type":"boolean","description":"Whether the document is archived. Archived documents stay in the project but are hidden from default views. Independent of deletion — an archived document is not deleted, and a deleted document is not archived."},"content":{"type":"string","description":"Initial document body, as Markdown. Supports headings, lists, tables, code blocks and Nifty mention syntax. Create-only: the body of an existing document is EXTENDED with the `append` operation and read back with the `content` operation — it can never be replaced or deleted, and it is not returned by list or get."},"projectId":{"type":["string","null"],"description":"The project this document belongs to. Required on create — a document always lives in a project — and immutable afterwards."},"parentDocumentId":{"type":["string","null"],"description":"The document this one is nested under — a parent in the same project. Set once at creation and immutable thereafter."},"folderId":{"type":["string","null"],"description":"The folder this document sits in — a document folder in the same project. Omit or send null to place it at the project root. The folder path is derived by the server. Set once at creation; moving a document between folders is not part of the update surface yet."},"size":{"type":"number","description":"Size of the uploaded file in bytes. Required on create — pass the byte count of what you uploaded. Maximum 2,147,483,646 (2 GB) per file, and the upload also has to fit your team's remaining storage quota; exceeding either is rejected."},"uploadKey":{"type":"string","description":"Storage key of the already-uploaded bytes: call the createUploadUrl operation, PUT the bytes to the presigned URL it returns, then pass its `uploadKey` here verbatim. Create REGISTERS metadata; it does not upload bytes. Write-only — read the file back through `url`."},"taskId":{"type":["string","null"],"description":"Attach the file to this task. At least one of `projectId`, `taskId`, `documentId`, `messageId` or `folderId` is required on create. Fixed at upload."},"documentId":{"type":["string","null"],"description":"Attach the file to this document. At least one of `projectId`, `taskId`, `documentId`, `messageId` or `folderId` is required on create. Fixed at upload."},"messageId":{"type":["string","null"],"description":"Attach the file to this message. At least one of `projectId`, `taskId`, `documentId`, `messageId` or `folderId` is required on create. Fixed at upload."},"find":{"type":"string","minLength":1,"description":"Exact text to locate in the document body (read it first with `content` — this must match verbatim, not a pattern). Must be non-empty."},"replace":{"type":"string","description":"Text that replaces the located `find` match. May be empty to delete the matched text outright. Plain text only — unlike `append`'s Markdown, no formatting/mentions are interpreted here; the replacement inherits the surrounding text's existing formatting."},"occurrence":{"type":"integer","exclusiveMinimum":0,"description":"When `find` matches more than once, replace only the Nth match (1-based). Omit to target the first match. Ignored when `replaceAll` is true."},"anchorBefore":{"type":"string","description":"Disambiguate a non-unique `find` by requiring this exact text to immediately PRECEDE the match. Use when `occurrence` is inconvenient (e.g. the match position may shift)."},"anchorAfter":{"type":"string","description":"Disambiguate a non-unique `find` by requiring this exact text to immediately FOLLOW the match. Can be combined with `anchorBefore` to pin both sides of an ambiguous match."},"replaceAll":{"type":"boolean","description":"Replace every occurrence of `find` in the document, not just one. When true, `occurrence` is ignored."},"contentType":{"type":"string","minLength":1,"maxLength":255,"description":"IANA media type to store. Omit and the server infers it — for uploadFromUrl from the fetched response Content-Type, otherwise from the content prefix or the name extension."},"contentBase64":{"type":"string","minLength":1,"maxLength":4300000,"description":"File bytes as base64 (standard or url-safe alphabet; an optional `data:<mime>;base64,` prefix is accepted). Max 3 MB decoded."},"sourceUrl":{"type":"string","minLength":1,"maxLength":2048,"description":"Public http/https URL the SERVER fetches the bytes from. Up to 3 redirects are followed (every hop is re-validated); max 25 MB; default ports only. URLs with embedded credentials are refused, as are private/internal addresses."}},"required":["resource","operation"],"additionalProperties":true,"$schema":"http://json-schema.org/draft-07/schema#"},"annotations":{"title":"Create or update docs and files","readOnlyHint":false,"destructiveHint":true,"idempotentHint":false,"openWorldHint":true},"execution":{"taskSupport":"forbidden"},"_meta":{"requiredScopes":["annotations:write","documents:link","documents:write","files:link","files:write"]}},{"name":"knowledge_destroy","description":"Delete knowledge resources: annotation (A drawing or comment-anchor placed on a file); document (A project-scoped rich-text document (Nifty native doc)); file (An uploaded file attached to a project, task, document, message, or folder). Pass {resource, operation: \"delete\", id}. Batch delete: bulkDelete — pass {resource, operation: \"bulkDelete\", ids:[…], onError?}. Bulk deletes are NOT idempotent-safe on retry: deleting an already-deleted row reports resource_not_found, not success. If the response is lost, re-query the ids to see what actually remains and delete only those — do not blind-retry the batch, and do not read a resource_not_found on a lost-response retry as \"the delete failed\".","inputSchema":{"type":"object","properties":{"resource":{"type":"string","enum":["annotation","document","file"],"description":"Which resource within this domain group to act on."},"operation":{"type":"string","enum":["bulkDelete","delete"],"description":"Which operation to run on the resource. Valid arguments depend on the (resource, operation) pair — unknown arguments are rejected with an error that names the accepted ones (the top-level schema is permissive because it forwards to a strict per-operation validator)."},"id":{"type":"string","pattern":"^[0-9A-Za-z_!]{1,16}$","description":"Entity id to delete."},"ids":{"type":"array","items":{"type":"string","pattern":"^[0-9A-Za-z_!]{1,16}$"},"minItems":1,"description":"bulkDelete only: entity ids to delete. Max 150."},"onError":{"type":"string","enum":["abort","continue"],"description":"bulk ops only: abort (default, stop at first failure) or continue (best-effort, per-item errors in the result)."}},"required":["resource","operation"],"additionalProperties":true,"$schema":"http://json-schema.org/draft-07/schema#"},"annotations":{"title":"Delete docs and files","readOnlyHint":false,"destructiveHint":true,"idempotentHint":true,"openWorldHint":false},"execution":{"taskSupport":"forbidden"},"_meta":{"requiredScopes":["annotations:delete","documents:delete","files:delete"]}},{"name":"admin_query","description":"Read (list / get / search) admin resources: auditEvent (A read-only, immutable audit-trail record of MCP tool-calls); customField (A custom-field DEFINITION (read-only): name, type, associations, and dropdown options); favorite (A sidebar favorite for the authenticated user); favoriteFolder (A user-created folder for organizing favorite items in the sidebar); label (A team-scoped tag used to categorize tasks, documents, files, and other workspace entities); role (A team-scoped permission set assigned to team members). Pass {resource, operation} plus the operation's args.","inputSchema":{"type":"object","properties":{"resource":{"type":"string","enum":["auditEvent","customField","favorite","favoriteFolder","label","role"],"description":"Which resource within this domain group to act on."},"operation":{"type":"string","enum":["get","list","search"],"description":"Which operation to run on the resource. Valid arguments depend on the (resource, operation) pair — unknown arguments are rejected with an error that names the accepted ones (the top-level schema is permissive because it forwards to a strict per-operation validator)."},"occurredAt":{"type":"string","description":"When the audited action occurred (event time)."},"occurredAt_gte":{"type":"string","description":"Filter \"occurredAt\" by gte (operator filter)."},"occurredAt_lte":{"type":"string","description":"Filter \"occurredAt\" by lte (operator filter)."},"action":{"type":"string","description":"The MCP tool that was invoked, for example `tasks_query` or `planning_mutate`. One row is written per tool-call, so this is the tool name — not the name of an underlying create or delete."},"action_in":{"type":"array","items":{"type":"string"},"description":"Filter \"action\" by in (operator filter)."},"resourceType":{"type":"string","description":"Always `mcp_tool_call` today: rows are written per MCP tool-call, not per resource that changed."},"resourceType_in":{"type":"array","items":{"type":"string"},"description":"Filter \"resourceType\" by in (operator filter)."},"source":{"type":"string","description":"A coarse bucket for who acted: `human`, `agent` or `system`. It is a summary of the finer actor classification (which also distinguishes cron, automations and recurring tasks) — so two events with different origins can both read `system`."},"source_in":{"type":"array","items":{"type":"string"},"description":"Filter \"source\" by in (operator filter)."},"actorId":{"type":"string","description":"Identifies who performed the action. It cannot be resolved to a person through this API yet — use `credentialName` for display, and treat this as a stable grouping key."},"actorId_in":{"type":"array","items":{"type":"string"},"description":"Filter \"actorId\" by in (operator filter)."},"credentialId":{"type":"string","description":"The credential the caller authenticated with. There is no endpoint that resolves it — use `credentialName` for display and this as an opaque grouping key."},"credentialId_in":{"type":"array","items":{"type":"string"},"description":"Filter \"credentialId\" by in (operator filter)."},"createdAt":{"type":"string","description":"Creation timestamp."},"createdAt_gt":{"type":"string","description":"Filter \"createdAt\" by gt (operator filter)."},"createdAt_gte":{"type":"string","description":"Filter \"createdAt\" by gte (operator filter)."},"createdAt_lt":{"type":"string","description":"Filter \"createdAt\" by lt (operator filter)."},"createdAt_lte":{"type":"string","description":"Filter \"createdAt\" by lte (operator filter)."},"name":{"type":"string","description":"Field name, for example \"Priority\" or \"T-shirt size\"."},"name_contains":{"type":"string","description":"Filter \"name\" by contains (operator filter)."},"name_startsWith":{"type":"string","description":"Filter \"name\" by startsWith (operator filter)."},"type":{"type":"string","description":"type — varies by resource; validated per (resource, operation) at call time"},"type_in":{"type":"array","items":{"type":"string"},"description":"Filter \"type\" by in (operator filter)."},"inLibrary":{"type":"boolean","description":"Whether this definition comes from the reusable field library rather than being bound to one workspace. Library definitions are not returned by this API today, so it is always false here."},"projectId":{"type":"string","description":"The project a `project` or `projectEmbed` favorite lives in. Required on create for those two types and ignored for the rest. Set once — a favorite cannot be re-pointed."},"projectId_in":{"type":"array","items":{"type":"string"},"description":"Filter \"projectId\" by in (operator filter)."},"favoriteFolderId":{"type":"string","description":"Folder grouping this favorite. Null = top-level (not in any folder). Mutable — favorites move between folders."},"favoriteFolderId_in":{"type":"array","items":{"type":"string"},"description":"Filter \"favoriteFolderId\" by in (operator filter)."},"purgeAt":{"type":"string","description":"When this resource will be permanently deleted — soft-delete time + 30-day retention; null while live."},"purgeAt_null":{"type":"string","description":"Filter \"purgeAt\" by null (operator filter)."},"purgeAt_notNull":{"type":"string","description":"Filter \"purgeAt\" by notNull (operator filter)."},"purgeAt_gt":{"type":"string","description":"Filter \"purgeAt\" by gt (operator filter)."},"purgeAt_gte":{"type":"string","description":"Filter \"purgeAt\" by gte (operator filter)."},"purgeAt_lt":{"type":"string","description":"Filter \"purgeAt\" by lt (operator filter)."},"purgeAt_lte":{"type":"string","description":"Filter \"purgeAt\" by lte (operator filter)."},"color":{"type":"string","description":"Hex color, `#RGB` or `#RRGGBB`. Omit it and the server assigns a random palette color, so a label always has one. Stored lowercase and expanded to 6 digits, so `#00A99B` reads back as `#00a99b`. Send at least one of `name` or `color` when creating a label."},"color_in":{"type":"array","items":{"type":"string"},"description":"Filter \"color\" by in (operator filter)."},"editable":{"type":"boolean","description":"False for the four built-in roles (owner, manager, regular, client). Those cannot be renamed, re-permissioned or deleted — only `initialScreen` may change."},"id":{"type":"string","pattern":"^[0-9A-Za-z_!]{1,16}$","description":"Entity id — required for \"get\" and for instance-target read verb ops."},"limit":{"type":"integer","exclusiveMinimum":0,"description":"Items per page for \"list\"/\"search\" (default 50). Over the server cap it is silently clamped, not rejected — a short page does NOT mean the end; page until `nextCursor` is null."},"cursor":{"type":"string","description":"Opaque pagination cursor returned by a previous \"list\"/\"search\"."},"includeTotal":{"type":"boolean","description":"Opt-in. List `total` is `null` by default for performance (an extra COUNT over the full set). Set true to get `total` = the count of the FULL filtered + ACL-scoped result set (NOT the current page; ignores limit/cursor). The `total` key is always present: integer when requested, null when not. To ask only 'how many', pass includeTotal:true with limit:1 so you don't over-fetch rows. List-only (search always returns total)."},"query":{"type":"string","description":"Full-text query — REQUIRED for operation \"search\". The \"list\" operation has NO free-text search: it filters only by the resource's filterable fields. To search by text, use operation:\"search\" with \"query\" (not a \"search\" key on \"list\")."},"expand":{"type":"string","description":"Comma-separated relations to expand inline on the returned entity/entities. Nested paths allowed with dots (e.g. members.member) up to depth 4; nesting into a relation also requires that target's read scope. Available per resource — customField: options/hiddenFor; favorite: member/favoriteFolder/project; favoriteFolder: createdBy/updatedBy; label: createdByActor/deletedBy; role: createdByActor. Unknown relations rejected."},"sort":{"type":"string","description":"Sort \"list\"/\"search\" results by ONE column. Descending: prefix \"-\" (e.g. \"-updatedAt\") OR suffix \":desc\" (e.g. \"updatedAt:desc\"). Ascending: bare column (e.g. \"updatedAt\") OR suffix \":asc\". A single string — NOT separate sortBy/sortDir, and do NOT combine \"-\" with \":asc\"/\":desc\". An unlisted column is rejected with an error naming the valid ones. Sortable columns + default per resource — auditEvent: occurredAt (default -occurredAt); customField: name/createdAt (default name); favorite: name/order/createdAt (default order); favoriteFolder: name/order/createdAt (default order); label: name/createdAt (default createdAt); role: name/createdAt (default name). Those defaults are LIST-only: omit sort on \"list\" to get the default shown, but omit it on \"search\" and results come back ranked by text relevance instead. Pass sort explicitly (e.g. sort=order) to force a specific order on either."}},"required":["resource","operation"],"additionalProperties":true,"$schema":"http://json-schema.org/draft-07/schema#"},"annotations":{"title":"Find labels, roles, favorites and custom fields","readOnlyHint":true,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false},"execution":{"taskSupport":"forbidden"},"_meta":{"requiredScopes":["audit-events:read","custom-fields:read","favorite-folders:read","favorites:read","labels:read","roles:read"]}},{"name":"admin_mutate","description":"Create or update admin resources: favorite (A sidebar favorite for the authenticated user); favoriteFolder (A user-created folder for organizing favorite items in the sidebar); label (A team-scoped tag used to categorize tasks, documents, files, and other workspace entities); role (A team-scoped permission set assigned to team members). Pass {resource, operation} plus the resource's writable fields. Batch writes: bulkCreate, bulkUpdate — pass {resource, operation, items:[…], onError?}. Recovering a bulkCreate depends on whether you got a result envelope back. GOT ONE: trust it — it is index-aligned, rows marked ok are created, and you may send a NEW batch containing only the rows that never got created. NO ENVELOPE (timeout / lost response): resend the SAME items in the SAME order — that exact batch is deduplicated per row and replays what already landed instead of re-creating it. Do NOT re-split it or drop items first; that changes the dedup key and DOES create duplicates. bulkUpdate carries no dedup key, so it is safe to resend only because each item is an absolute field set — never resend one whose values are relative to the current value. Concurrency-safe updates: on a single-row update, pass expectedUpdatedAt = the updatedAt from the read your edit is based on; a stale_write (409) means the row changed since — re-read it, re-derive your change, and retry with the fresh updatedAt (never a timestamp echoed by the error). The guard requires at least one field change. Bulk items do not accept it: with onError absent, one item carrying it rejects the WHOLE batch client-side (0 HTTP calls); onError:'continue' ships the valid rows and returns per-item errors. Clearing a value: a field whose type includes \"null\" accepts an explicit JSON null to clear it (for an FK, to unlink it). Omitting a key leaves that field UNCHANGED — omission and null are different requests, so send only the keys you mean to change.","inputSchema":{"type":"object","properties":{"resource":{"type":"string","enum":["favorite","favoriteFolder","label","role"],"description":"Which resource within this domain group to act on."},"operation":{"type":"string","enum":["bulkCreate","bulkUpdate","create","update"],"description":"Which operation to run on the resource. Valid arguments depend on the (resource, operation) pair — unknown arguments are rejected with an error that names the accepted ones (the top-level schema is permissive because it forwards to a strict per-operation validator)."},"id":{"type":"string","pattern":"^[0-9A-Za-z_!]{1,16}$","description":"Entity id — for \"update\", the add{Relation}/remove{Relation} ops, and instance-target verb ops (id = the entity acted on)."},"ids":{"type":"array","items":{"type":"string","pattern":"^[0-9A-Za-z_!]{1,16}$","description":"related entity id"},"description":"Target ids for an add{Relation}/remove{Relation} op (1..500; auto-chunked)."},"items":{"type":"array","items":{"type":"object","properties":{},"additionalProperties":true},"minItems":1,"description":"bulkCreate/bulkUpdate only: array of per-item bodies, each the same shape as a single create/update (bulkUpdate items include id). Max 150."},"onError":{"type":"string","enum":["abort","continue"],"description":"bulk ops only: abort (default, stop at first failure) or continue (best-effort, per-item errors in the result)."},"expectedUpdatedAt":{"type":"string","format":"date-time","description":"update (single-row) only: concurrency guard — the `updatedAt` from the read your edit is based on. 409 stale_write if the row changed since; re-read, re-derive, retry with the fresh value. Requires ≥1 field change. Not accepted on bulk items."},"name":{"type":"string","description":"The label shown in the sidebar. It is a snapshot you supply, NOT a live mirror — rename the project and the favorite keeps the old label until you update it yourself."},"order":{"type":["number","null"],"description":"Rank within the sidebar. Omit it on create to append at the end (the server assigns the next rank); supply one to place it explicitly. Settable on update for drag-and-drop reordering — pick a value between the two neighbours you are dropping between."},"type":{"type":"string","description":"type — varies by resource; validated per (resource, operation) at call time"},"resourceId":{"type":"string","description":"The id of the favorited item. Required for every `type` except the three standing personal views (`personalNotes`, `personalTasks`, `createdByMe`), which take none. Set once — a favorite cannot be re-pointed."},"projectId":{"type":"string","description":"The project a `project` or `projectEmbed` favorite lives in. Required on create for those two types and ignored for the rest. Set once — a favorite cannot be re-pointed."},"favoriteFolderId":{"type":["string","null"],"description":"Folder grouping this favorite. Null = top-level (not in any folder). Mutable — favorites move between folders."},"color":{"type":"string","description":"Hex color, `#RGB` or `#RRGGBB`. Omit it and the server assigns a random palette color, so a label always has one. Stored lowercase and expanded to 6 digits, so `#00A99B` reads back as `#00a99b`. Send at least one of `name` or `color` when creating a label."},"permissions":{"type":"array","items":{},"description":"The permissions this role grants. Send names; an unknown name is rejected with 400, and omitting the field (or sending `[]`) means the role grants nothing. **You may only grant permissions you hold yourself**; on update this is judged on the delta, so permissions the role already had can be carried forward untouched."},"initialScreen":{"type":"string","enum":["default","allTasks","createTask","calendar","overview","workloads","all_tasks","create_task"],"description":"The screen members holding this role land on when they open the workspace."}},"required":["resource","operation"],"additionalProperties":true,"$schema":"http://json-schema.org/draft-07/schema#"},"annotations":{"title":"Create or update labels, roles and favorites","readOnlyHint":false,"destructiveHint":true,"idempotentHint":false,"openWorldHint":false},"execution":{"taskSupport":"forbidden"},"_meta":{"requiredScopes":["favorite-folders:write","favorites:write","labels:write","roles:write"]}},{"name":"admin_destroy","description":"Delete admin resources: favorite (A sidebar favorite for the authenticated user); favoriteFolder (A user-created folder for organizing favorite items in the sidebar); label (A team-scoped tag used to categorize tasks, documents, files, and other workspace entities); role (A team-scoped permission set assigned to team members). Pass {resource, operation: \"delete\", id}. Batch delete: bulkDelete — pass {resource, operation: \"bulkDelete\", ids:[…], onError?}. Bulk deletes are NOT idempotent-safe on retry: deleting an already-deleted row reports resource_not_found, not success. If the response is lost, re-query the ids to see what actually remains and delete only those — do not blind-retry the batch, and do not read a resource_not_found on a lost-response retry as \"the delete failed\".","inputSchema":{"type":"object","properties":{"resource":{"type":"string","enum":["favorite","favoriteFolder","label","role"],"description":"Which resource within this domain group to act on."},"operation":{"type":"string","enum":["bulkDelete","delete"],"description":"Which operation to run on the resource. Valid arguments depend on the (resource, operation) pair — unknown arguments are rejected with an error that names the accepted ones (the top-level schema is permissive because it forwards to a strict per-operation validator)."},"id":{"type":"string","pattern":"^[0-9A-Za-z_!]{1,16}$","description":"Entity id to delete."},"ids":{"type":"array","items":{"type":"string","pattern":"^[0-9A-Za-z_!]{1,16}$"},"minItems":1,"description":"bulkDelete only: entity ids to delete. Max 150."},"onError":{"type":"string","enum":["abort","continue"],"description":"bulk ops only: abort (default, stop at first failure) or continue (best-effort, per-item errors in the result)."}},"required":["resource","operation"],"additionalProperties":true,"$schema":"http://json-schema.org/draft-07/schema#"},"annotations":{"title":"Delete labels, roles and favorites","readOnlyHint":false,"destructiveHint":true,"idempotentHint":true,"openWorldHint":false},"execution":{"taskSupport":"forbidden"},"_meta":{"requiredScopes":["favorite-folders:delete","favorites:delete","labels:delete","roles:delete"]}},{"name":"communication_query","description":"Read (list / get / search) communication resources: chat (A team-scoped conversation (group chat, direct message, or project general-discussion)); checkIn (A team-scoped recurring or ad-hoc check-in attached to a form); checkInRequest (One scheduled or manually triggered occurrence of a CheckIn); message (A message in a chat, task comment thread, document comment, file comment, annotation thread, or message thread (reply)). Pass {resource, operation} plus the operation's args. A 1:1 direct chat has no name, so never hunt one by listing or sorting chats: {resource:\"chat\", operation:\"list\", recipientMemberId:<a TeamMember id>} returns the caller's direct chat with that member. SENDING a DM needs no chat id at all — communication_mutate message create takes recipientMemberId directly.","inputSchema":{"type":"object","properties":{"resource":{"type":"string","enum":["chat","checkIn","checkInRequest","message"],"description":"Which resource within this domain group to act on."},"operation":{"type":"string","enum":["get","list","search"],"description":"Which operation to run on the resource. Valid arguments depend on the (resource, operation) pair — unknown arguments are rejected with an error that names the accepted ones (the top-level schema is permissive because it forwards to a strict per-operation validator)."},"createdAt":{"type":"string","description":"Creation timestamp."},"createdAt_gt":{"type":"string","description":"Filter \"createdAt\" by gt (operator filter)."},"createdAt_gte":{"type":"string","description":"Filter \"createdAt\" by gte (operator filter)."},"createdAt_lt":{"type":"string","description":"Filter \"createdAt\" by lt (operator filter)."},"createdAt_lte":{"type":"string","description":"Filter \"createdAt\" by lte (operator filter)."},"name":{"type":"string","description":"Chat display name. Required for group chats (the only v3-creatable kind); NULL on direct chats and the team self-chat."},"name_contains":{"type":"string","description":"Filter \"name\" by contains (operator filter)."},"name_startsWith":{"type":"string","description":"Filter \"name\" by startsWith (operator filter)."},"type":{"type":"string","description":"type — varies by resource; validated per (resource, operation) at call time"},"type_in":{"type":"array","items":{"type":"string"},"description":"Filter \"type\" by in (operator filter)."},"lastMessageAt":{"type":"string","description":"Timestamp of the latest message (server-bumped by the message-create afterCommit hook). NULL until the first message. List default-sorts on it."},"lastMessageAt_gt":{"type":"string","description":"Filter \"lastMessageAt\" by gt (operator filter)."},"lastMessageAt_gte":{"type":"string","description":"Filter \"lastMessageAt\" by gte (operator filter)."},"lastMessageAt_lt":{"type":"string","description":"Filter \"lastMessageAt\" by lt (operator filter)."},"lastMessageAt_lte":{"type":"string","description":"Filter \"lastMessageAt\" by lte (operator filter)."},"recipientMemberId":{"type":"string","description":"Return your own 1:1 direct chat with this team member, if it exists. Filter-only — it is not part of the create/update body and is never returned on a chat."},"updatedAt":{"type":"string","description":"Last update timestamp."},"updatedAt_gt":{"type":"string","description":"Filter \"updatedAt\" by gt (operator filter)."},"updatedAt_gte":{"type":"string","description":"Filter \"updatedAt\" by gte (operator filter)."},"updatedAt_lt":{"type":"string","description":"Filter \"updatedAt\" by lt (operator filter)."},"updatedAt_lte":{"type":"string","description":"Filter \"updatedAt\" by lte (operator filter)."},"createdByActorId":{"type":"string","description":"Who created this record — a person, an agent, an automation or the system itself. Read-only, stamped by the server on writes made through this API, and null on records created any other way, so treat it as present-or-null rather than always populated. This is an actor id (a UUID), not a member id: look it up with `GET /actors/{id}`, whose `memberId` gives you the person to use against `/members`."},"createdByActorId_in":{"type":"array","items":{"type":"string"},"description":"Filter \"createdByActorId\" by in (operator filter)."},"requestType":{"type":"string","enum":["schedule","manual"],"description":"What triggers this check-in. `schedule` runs it on a recurrence and REQUIRES `recurringRuleId`; `manual` runs it on demand and must omit `recurringRuleId`."},"requestType_in":{"type":"array","items":{"type":"string"},"description":"Filter \"requestType\" by in (operator filter)."},"formId":{"type":"string","description":"The form holding this check-in's questions. Set once when the check-in is created."},"formId_in":{"type":"array","items":{"type":"string"},"description":"Filter \"formId\" by in (operator filter)."},"recurringRuleId":{"type":"string","description":"Identifier of the recurrence rule that triggers this check-in, or null when `requestType` is `manual`. Create the rule first with `POST /recurring-rules` and pass its id here, or send an inline `schedule` object on the create body instead and one will be created for you."},"recurringRuleId_in":{"type":"array","items":{"type":"string"},"description":"Filter \"recurringRuleId\" by in (operator filter)."},"checkInId":{"type":"string","description":"The check-in this request belongs to. Required when creating one, and immutable afterwards — the parent check-in decides both the workspace it lives in and the form that is answered."},"checkInId_in":{"type":"array","items":{"type":"string"},"description":"Filter \"checkInId\" by in (operator filter)."},"reminderSentAt":{"type":"string","description":"When the most recent reminder for this request was sent, or `null` if no reminder has been sent yet. Server-managed (reminders are rate-limited to one every 8 hours) and read-only."},"reminderSentAt_gt":{"type":"string","description":"Filter \"reminderSentAt\" by gt (operator filter)."},"reminderSentAt_gte":{"type":"string","description":"Filter \"reminderSentAt\" by gte (operator filter)."},"reminderSentAt_lt":{"type":"string","description":"Filter \"reminderSentAt\" by lt (operator filter)."},"reminderSentAt_lte":{"type":"string","description":"Filter \"reminderSentAt\" by lte (operator filter)."},"purgeAt":{"type":"string","description":"When this resource will be permanently deleted — soft-delete time + 30-day retention; null while live."},"purgeAt_null":{"type":"string","description":"Filter \"purgeAt\" by null (operator filter)."},"purgeAt_notNull":{"type":"string","description":"Filter \"purgeAt\" by notNull (operator filter)."},"purgeAt_gt":{"type":"string","description":"Filter \"purgeAt\" by gt (operator filter)."},"purgeAt_gte":{"type":"string","description":"Filter \"purgeAt\" by gte (operator filter)."},"purgeAt_lt":{"type":"string","description":"Filter \"purgeAt\" by lt (operator filter)."},"purgeAt_lte":{"type":"string","description":"Filter \"purgeAt\" by lte (operator filter)."},"subtype":{"type":"string","description":"Non-null on the system/lifecycle messages the server generates (task assigned, label applied, member joined a chat). Null on messages a person or an agent wrote. Read-only — you cannot create a system message through the API. A few values are not self-explanatory: `hello` is the welcome message posted in a new chat, `tieTask` / `untieTask` record a dependency being added or removed between two tasks, and `moveSubtask` records a subtask being re-parented."},"subtype_in":{"type":"array","items":{"type":"string"},"description":"Filter \"subtype\" by in (operator filter)."},"subtype_null":{"type":"string","description":"Filter \"subtype\" by null (operator filter)."},"subtype_notNull":{"type":"string","description":"Filter \"subtype\" by notNull (operator filter)."},"isEdited":{"type":"boolean","description":"True when the text has been edited at least once. Server-managed by the edit path."},"authorId":{"type":"string","description":"The team member who wrote this message — a member id, so resolve it with `?expand=author` or `GET /members/{id}`. Null for system and app-mirrored messages — use `createdByActorType` to tell an agent-written message from a human one."},"authorId_in":{"type":"array","items":{"type":"string"},"description":"Filter \"authorId\" by in (operator filter)."},"chatId":{"type":"string","description":"The chat this message belongs to (group, direct, or a project discussion). Exactly one parent context per message."},"chatId_in":{"type":"array","items":{"type":"string"},"description":"Filter \"chatId\" by in (operator filter)."},"taskId":{"type":"string","description":"The task this message comments on. Exactly one parent context per message."},"taskId_in":{"type":"array","items":{"type":"string"},"description":"Filter \"taskId\" by in (operator filter)."},"documentId":{"type":"string","description":"The document this message comments on. Exactly one parent context per message."},"documentId_in":{"type":"array","items":{"type":"string"},"description":"Filter \"documentId\" by in (operator filter)."},"fileId":{"type":"string","description":"The file this message comments on. Exactly one parent context per message — `fileId` and `annotationId` may be sent together (a comment on an annotation drawn on a file)."},"fileId_in":{"type":"array","items":{"type":"string"},"description":"Filter \"fileId\" by in (operator filter)."},"annotationId":{"type":"string","description":"The annotation this message is threaded under. Exactly one parent context per message — `fileId` and `annotationId` may be sent together (a comment on an annotation drawn on a file)."},"annotationId_in":{"type":"array","items":{"type":"string"},"description":"Filter \"annotationId\" by in (operator filter)."},"parentMessageId":{"type":"string","description":"The message this one replies to. Exactly one parent context per message."},"parentMessageId_in":{"type":"array","items":{"type":"string"},"description":"Filter \"parentMessageId\" by in (operator filter)."},"sharedDocumentId":{"type":"string","description":"A document shared into a chat message. Distinct from `documentId`, which marks a comment ON a document. Read-only — share a document from the Nifty app."},"sharedDocumentId_in":{"type":"array","items":{"type":"string"},"description":"Filter \"sharedDocumentId\" by in (operator filter)."},"id":{"type":"string","pattern":"^[0-9A-Za-z_!]{1,16}$","description":"Entity id — required for \"get\" and for instance-target read verb ops."},"limit":{"type":"integer","exclusiveMinimum":0,"description":"Items per page for \"list\"/\"search\" (default 50). Over the server cap it is silently clamped, not rejected — a short page does NOT mean the end; page until `nextCursor` is null."},"cursor":{"type":"string","description":"Opaque pagination cursor returned by a previous \"list\"/\"search\"."},"includeTotal":{"type":"boolean","description":"Opt-in. List `total` is `null` by default for performance (an extra COUNT over the full set). Set true to get `total` = the count of the FULL filtered + ACL-scoped result set (NOT the current page; ignores limit/cursor). The `total` key is always present: integer when requested, null when not. To ask only 'how many', pass includeTotal:true with limit:1 so you don't over-fetch rows. List-only (search always returns total)."},"query":{"type":"string","description":"Full-text query — REQUIRED for operation \"search\". The \"list\" operation has NO free-text search: it filters only by the resource's filterable fields. To search by text, use operation:\"search\" with \"query\" (not a \"search\" key on \"list\")."},"expand":{"type":"string","description":"Comma-separated relations to expand inline on the returned entity/entities. Nested paths allowed with dots (e.g. members.member) up to depth 4; nesting into a relation also requires that target's read scope. Available per resource — chat: members/project/createdByActor/updatedByActor; checkIn: form/recurringRule/createdByActor/assignees/subscribers/requests; checkInRequest: checkIn/assignees/respondedBy; message: author/deletedBy/createdByActor/updatedByActor/chat/task/document/file/annotation/parentMessage/sharedDocument/tagged/seenBy/hiddenFor/files/replies. Unknown relations rejected. A message's images and files are NOT in `text` — they are file rows under `files`, which every message read auto-includes when expand is omitted, so an empty `text` with an empty `files` genuinely means no content. Expanded rows are file OBJECTS (name/url/size); the write side takes an array of file IDs, so do not echo a read straight back on update. A message can carry a THREAD: its replies are message rows under `replies`, auto-included on every message read when expand is omitted (one level — a reply has no replies of its own) and silently omitted when you pass expand explicitly."},"sort":{"type":"string","description":"Sort \"list\"/\"search\" results by ONE column. Descending: prefix \"-\" (e.g. \"-updatedAt\") OR suffix \":desc\" (e.g. \"updatedAt:desc\"). Ascending: bare column (e.g. \"updatedAt\") OR suffix \":asc\". A single string — NOT separate sortBy/sortDir, and do NOT combine \"-\" with \":asc\"/\":desc\". An unlisted column is rejected with an error naming the valid ones. Sortable columns + default per resource — chat: name/lastMessageAt/createdAt (default lastMessageAt); checkIn: name/createdAt/updatedAt (default -createdAt); checkInRequest: createdAt/updatedAt/reminderSentAt (default -createdAt); message: createdAt/updatedAt (default -createdAt). Those defaults are LIST-only: omit sort on \"list\" to get the default shown, but omit it on \"search\" and results come back ranked by text relevance instead. Pass sort explicitly (e.g. sort=order) to force a specific order on either."}},"required":["resource","operation"],"additionalProperties":true,"$schema":"http://json-schema.org/draft-07/schema#"},"annotations":{"title":"Find chats, messages and check-ins","readOnlyHint":true,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false},"execution":{"taskSupport":"forbidden"},"_meta":{"requiredScopes":["chats:read","check-in-requests:read","check-ins:read","messages:read"]}},{"name":"communication_mutate","description":"Create or update communication resources: chat (A team-scoped conversation (group chat, direct message, or project general-discussion)); checkIn (A team-scoped recurring or ad-hoc check-in attached to a form); checkInRequest (One scheduled or manually triggered occurrence of a CheckIn); message (A message in a chat, task comment thread, document comment, file comment, annotation thread, or message thread (reply)). Pass {resource, operation} plus the resource's writable fields. Also links/unlinks relations via addAssignees, addSubscribers, removeAssignees, removeSubscribers — pass {resource, operation, id, ids}. Batch writes: bulkCreate, bulkUpdate — pass {resource, operation, items:[…], onError?}. Recovering a bulkCreate depends on whether you got a result envelope back. GOT ONE: trust it — it is index-aligned, rows marked ok are created, and you may send a NEW batch containing only the rows that never got created. NO ENVELOPE (timeout / lost response): resend the SAME items in the SAME order — that exact batch is deduplicated per row and replays what already landed instead of re-creating it. Do NOT re-split it or drop items first; that changes the dedup key and DOES create duplicates. bulkUpdate carries no dedup key, so it is safe to resend only because each item is an absolute field set — never resend one whose values are relative to the current value. Concurrency-safe updates: on a single-row update, pass expectedUpdatedAt = the updatedAt from the read your edit is based on; a stale_write (409) means the row changed since — re-read it, re-derive your change, and retry with the fresh updatedAt (never a timestamp echoed by the error). The guard requires at least one field change. Bulk items do not accept it: with onError absent, one item carrying it rejects the WHOLE batch client-side (0 HTTP calls); onError:'continue' ships the valid rows and returns per-item errors. Clearing a value: a field whose type includes \"null\" accepts an explicit JSON null to clear it (for an FK, to unlink it). Omitting a key leaves that field UNCHANGED — omission and null are different requests, so send only the keys you mean to change. Default to the surface you are working in (on a task → comment on that task). Before DMing a person or opening a discussion, take the channel from the user's intent; if it is unstated, verify rather than guess. To DM a person, send it directly — do NOT look up or create a chat first: {resource:\"message\", operation:\"create\", recipientMemberId:<a TeamMember id>, text} resolves-or-creates the caller's 1:1 chat with that member and posts there. A message-create response may include `created`: it refers to the DIRECT CHAT when DMing via recipientMemberId (true = a new 1:1 chat was minted, false = an existing one was reused) — NOT to the message. A success response means the message itself was created either way; do not re-send or re-verify on `created: false`.","inputSchema":{"type":"object","properties":{"resource":{"type":"string","enum":["chat","checkIn","checkInRequest","message"],"description":"Which resource within this domain group to act on."},"operation":{"type":"string","enum":["addAssignees","addSubscribers","bulkCreate","bulkUpdate","create","removeAssignees","removeSubscribers","update"],"description":"Which operation to run on the resource. Valid arguments depend on the (resource, operation) pair — unknown arguments are rejected with an error that names the accepted ones (the top-level schema is permissive because it forwards to a strict per-operation validator)."},"id":{"type":"string","pattern":"^[0-9A-Za-z_!]{1,16}$","description":"Entity id — for \"update\", the add{Relation}/remove{Relation} ops, and instance-target verb ops (id = the entity acted on)."},"ids":{"type":"array","items":{"type":"string","pattern":"^[0-9A-Za-z_!]{1,16}$","description":"related entity id"},"description":"Target ids for an add{Relation}/remove{Relation} op (1..500; auto-chunked)."},"items":{"type":"array","items":{"type":"object","properties":{},"additionalProperties":true},"minItems":1,"description":"bulkCreate/bulkUpdate only: array of per-item bodies, each the same shape as a single create/update (bulkUpdate items include id). Max 150."},"onError":{"type":"string","enum":["abort","continue"],"description":"bulk ops only: abort (default, stop at first failure) or continue (best-effort, per-item errors in the result)."},"expectedUpdatedAt":{"type":"string","format":"date-time","description":"update (single-row) only: concurrency guard — the `updatedAt` from the read your edit is based on. 409 stale_write if the row changed since; re-read, re-derive, retry with the fresh value. Requires ≥1 field change. Not accepted on bulk items."},"name":{"type":"string","description":"Chat display name. Required for group chats (the only v3-creatable kind); NULL on direct chats and the team self-chat."},"description":{"type":["string","null"],"description":"Optional free-text description. Supports Markdown. Embed refs: `<@memberId>` person, `<?taskId>` task, `<#projectId>` project, `<??taskGroupId>` status, `<^milestoneId>` list, `<~labelId>` label, `<$subteamId>` portfolio, `<!discussionId>` discussion. A project niceId + number (e.g. `PROJ-123`) auto-links that task; `:shortcode:` renders an emoji. Write mention/ref glyphs as plain literal text — wrapping one in backticks or other code formatting renders it as inert code instead of a resolved mention/link."},"type":{"type":"string","description":"type — varies by resource; validated per (resource, operation) at call time"},"memberIds":{"type":"array","items":{},"description":"Team members to add to the chat, by id — you are always included as its owner. Members are set here, when the chat is created, and cannot be changed afterwards, so send the full roster now. Write-only; read the resulting membership back with `?expand=members`."},"notifyTypes":{"type":"array","items":{},"description":"How assignees are reminded: `email`, `message` (an in-app message) or `notification` (a push notification). Every channel you list fires. Usually non-empty, but older records can come back with an empty list — read it, and if you are echoing a record back on an update, drop the key rather than sending the empty array (writes require at least one channel and will reject it)."},"requestType":{"type":"string","enum":["schedule","manual"],"description":"What triggers this check-in. `schedule` runs it on a recurrence and REQUIRES `recurringRuleId`; `manual` runs it on demand and must omit `recurringRuleId`."},"formId":{"type":"string","description":"The form holding this check-in's questions. Set once when the check-in is created."},"recurringRuleId":{"type":["string","null"],"description":"Identifier of the recurrence rule that triggers this check-in, or null when `requestType` is `manual`. Create the rule first with `POST /recurring-rules` and pass its id here, or send an inline `schedule` object on the create body instead and one will be created for you."},"schedule":{"type":"string","description":"Define the recurrence inline instead of pre-creating a rule: send the same timetable object `recurringRule.schedule` carries and a `recurringRule` is created in the same transaction and linked, so a failure anywhere rolls both back. This is the one-call alternative to `recurringRuleId` and mutually exclusive with it. Accepted on create only — change the schedule of an existing check-in by pointing `recurringRuleId` at another rule."},"assignees":{"type":"array","items":{"type":"string","minLength":1},"description":"Team members who must answer this check-in. Settable on create and update (whole-set replace), read back with `?expand=assignees`, or edited one at a time via `POST /check-ins/{id}/assignees` and `POST /check-ins/{id}/assignees/remove`."},"subscribers":{"type":"array","items":{"type":"string","minLength":1},"description":"Team members notified when someone answers, without being asked themselves. Settable on create and update (whole-set replace), read back with `?expand=subscribers`, or edited one at a time via `POST /check-ins/{id}/subscribers` and `POST /check-ins/{id}/subscribers/remove`."},"checkInId":{"type":"string","description":"The check-in this request belongs to. Required when creating one, and immutable afterwards — the parent check-in decides both the workspace it lives in and the form that is answered."},"recipientMemberId":{"type":"string","description":"DM recipient (a TeamMember id). Write-only \"send a DM\" convenience: set INSTEAD of a context FK; the backend resolves-or-creates the 1:1 direct chat between the caller and this member and posts there. Create-only; no column."},"text":{"type":["string","null"],"description":"Message body, up to 655,350 characters. Supports Markdown formatting, plus embedded mention tags (`<@memberId>` person, `<?taskId>` task, `<#projectId>` project, `<??statusId>` status, `<^listId>` list, `<~labelId>` label, `<$portfolioId>` portfolio, `<!discussionId>` discussion). Token glyphs are the literal wire ref format. Nullable for non-text messages. A project niceId + number (e.g. `PROJ-123`) auto-links that task; `:shortcode:` renders an emoji. Write mention/ref glyphs as plain literal text — wrapping one in backticks or other code formatting renders it as inert code instead of a resolved mention/link. If this is a task comment, keep it short and human: what changed, what you need from someone. Working context goes in a `taskHandoff`, not the comment."},"url":{"type":["string","null"],"description":"External URL for `type=gif` messages."},"hideLinkPreview":{"type":"boolean","description":"When true, link previews are not rendered for URLs in this message."},"audioDuration":{"type":["number","null"],"description":"Audio playback duration in seconds — populated for `type=audio` messages."},"entityKey":{"type":["string","null"],"description":"Grouping key for annotation threads inside the document editor."},"chatId":{"type":["string","null"],"description":"The chat this message belongs to (group, direct, or a project discussion). Exactly one parent context per message."},"taskId":{"type":["string","null"],"description":"The task this message comments on. Exactly one parent context per message."},"documentId":{"type":["string","null"],"description":"The document this message comments on. Exactly one parent context per message."},"fileId":{"type":["string","null"],"description":"The file this message comments on. Exactly one parent context per message — `fileId` and `annotationId` may be sent together (a comment on an annotation drawn on a file)."},"annotationId":{"type":["string","null"],"description":"The annotation this message is threaded under. Exactly one parent context per message — `fileId` and `annotationId` may be sent together (a comment on an annotation drawn on a file)."},"parentMessageId":{"type":["string","null"],"description":"The message this one replies to. Exactly one parent context per message."},"tagged":{"type":"array","items":{"type":"string"},"description":"Team members to @-mention on this message, by id. Accepted when the message is created; the mention list of an existing message is not editable, so it is not part of the update body. Read them back with `?expand=tagged`."},"files":{"type":"array","items":{"type":"string"},"description":"Existing files attached to this message, by id. Writable on create and on update: on update the array you send becomes the COMPLETE set, so a file dropped from it is detached (the file itself is not deleted). Omit the key to leave it untouched. Read them back with `?expand=files`."}},"required":["resource","operation"],"additionalProperties":true,"$schema":"http://json-schema.org/draft-07/schema#"},"annotations":{"title":"Create or update chats, messages and check-ins","readOnlyHint":false,"destructiveHint":true,"idempotentHint":false,"openWorldHint":false},"execution":{"taskSupport":"forbidden"},"_meta":{"requiredScopes":["chats:write","check-in-requests:link","check-in-requests:write","check-ins:link","check-ins:write","messages:write"]}},{"name":"communication_destroy","description":"Delete communication resources: chat (A team-scoped conversation (group chat, direct message, or project general-discussion)); checkIn (A team-scoped recurring or ad-hoc check-in attached to a form); checkInRequest (One scheduled or manually triggered occurrence of a CheckIn); message (A message in a chat, task comment thread, document comment, file comment, annotation thread, or message thread (reply)). Pass {resource, operation: \"delete\", id}. Batch delete: bulkDelete — pass {resource, operation: \"bulkDelete\", ids:[…], onError?}. Bulk deletes are NOT idempotent-safe on retry: deleting an already-deleted row reports resource_not_found, not success. If the response is lost, re-query the ids to see what actually remains and delete only those — do not blind-retry the batch, and do not read a resource_not_found on a lost-response retry as \"the delete failed\".","inputSchema":{"type":"object","properties":{"resource":{"type":"string","enum":["chat","checkIn","checkInRequest","message"],"description":"Which resource within this domain group to act on."},"operation":{"type":"string","enum":["bulkDelete","delete"],"description":"Which operation to run on the resource. Valid arguments depend on the (resource, operation) pair — unknown arguments are rejected with an error that names the accepted ones (the top-level schema is permissive because it forwards to a strict per-operation validator)."},"id":{"type":"string","pattern":"^[0-9A-Za-z_!]{1,16}$","description":"Entity id to delete."},"ids":{"type":"array","items":{"type":"string","pattern":"^[0-9A-Za-z_!]{1,16}$"},"minItems":1,"description":"bulkDelete only: entity ids to delete. Max 150."},"onError":{"type":"string","enum":["abort","continue"],"description":"bulk ops only: abort (default, stop at first failure) or continue (best-effort, per-item errors in the result)."}},"required":["resource","operation"],"additionalProperties":true,"$schema":"http://json-schema.org/draft-07/schema#"},"annotations":{"title":"Delete chats, messages and check-ins","readOnlyHint":false,"destructiveHint":true,"idempotentHint":true,"openWorldHint":false},"execution":{"taskSupport":"forbidden"},"_meta":{"requiredScopes":["chats:delete","check-in-requests:delete","check-ins:delete","messages:delete"]}},{"name":"tasks_query","description":"Read (list / get / search) tasks resources: checklist (A named, ordered container of checklist items on a task); checklistItem (A single completable line inside a checklist); reminder (A scheduled reminder anchored to a task); task (A unit of work in a project); taskHandoff (Working context for whoever picks a task up next — read the newest one of your kind before working a task, create one when you stop); tasksLayout (A saved task-board VIEW/LAYOUT for a user (per-user, optionally per-project): the board type + a `config` object (filters, grouping, sort, hidden columns, calendar range, etc.)). Pass {resource, operation} plus the operation's args. For project-scoped resources here (task): \"search\" REQUIRES `projectId` (no workspace-wide fallback — omitting it errors `project_not_specified`); \"list\" does NOT require it (omitting it falls back to a workspace-wide scan).","inputSchema":{"type":"object","properties":{"resource":{"type":"string","enum":["checklist","checklistItem","reminder","task","taskHandoff","tasksLayout"],"description":"Which resource within this domain group to act on."},"operation":{"type":"string","enum":["get","list","search"],"description":"Which operation to run on the resource. Valid arguments depend on the (resource, operation) pair — unknown arguments are rejected with an error that names the accepted ones (the top-level schema is permissive because it forwards to a strict per-operation validator)."},"createdAt":{"type":"string","description":"Creation timestamp."},"createdAt_gt":{"type":"string","description":"Filter \"createdAt\" by gt (operator filter)."},"createdAt_gte":{"type":"string","description":"Filter \"createdAt\" by gte (operator filter)."},"createdAt_lt":{"type":"string","description":"Filter \"createdAt\" by lt (operator filter)."},"createdAt_lte":{"type":"string","description":"Filter \"createdAt\" by lte (operator filter)."},"name":{"type":"string","description":"Checklist name — a group of steps on the task."},"name_contains":{"type":"string","description":"Filter \"name\" by contains (operator filter)."},"name_startsWith":{"type":"string","description":"Filter \"name\" by startsWith (operator filter)."},"taskId":{"type":"string","description":"Task this checklist belongs to. Required on create; immutable afterwards."},"taskId_in":{"type":"array","items":{"type":"string"},"description":"Filter \"taskId\" by in (operator filter)."},"completed":{"type":"boolean","description":"Whether the item is checked off. Defaults to false."},"checklistId":{"type":"string","description":"Checklist this item belongs to. Required on create; immutable afterwards."},"checklistId_in":{"type":"array","items":{"type":"string"},"description":"Filter \"checklistId\" by in (operator filter)."},"date":{"type":"string","description":"When the reminder fires, as an ISO 8601 instant (not a calendar date). Must be at least five minutes in the future. This is the only updatable field."},"date_gt":{"type":"string","description":"Filter \"date\" by gt (operator filter)."},"date_gte":{"type":"string","description":"Filter \"date\" by gte (operator filter)."},"date_lt":{"type":"string","description":"Filter \"date\" by lt (operator filter)."},"date_lte":{"type":"string","description":"Filter \"date\" by lte (operator filter)."},"type":{"type":"string","description":"type — varies by resource; validated per (resource, operation) at call time"},"type_in":{"type":"array","items":{"type":"string"},"description":"Filter \"type\" by in (operator filter)."},"updatedAt":{"type":"string","description":"Last update timestamp."},"updatedAt_gt":{"type":"string","description":"Filter \"updatedAt\" by gt (operator filter)."},"updatedAt_gte":{"type":"string","description":"Filter \"updatedAt\" by gte (operator filter)."},"updatedAt_lt":{"type":"string","description":"Filter \"updatedAt\" by lt (operator filter)."},"updatedAt_lte":{"type":"string","description":"Filter \"updatedAt\" by lte (operator filter)."},"createdByActorId":{"type":"string","description":"Who created this record — a person, an agent, an automation or the system itself. Read-only, stamped by the server on writes made through this API, and null on records created any other way, so treat it as present-or-null rather than always populated. This is an actor id (a UUID), not a member id: look it up with `GET /actors/{id}`, whose `memberId` gives you the person to use against `/members`."},"createdByActorId_in":{"type":"array","items":{"type":"string"},"description":"Filter \"createdByActorId\" by in (operator filter)."},"updatedByActorId":{"type":"string","description":"Who last wrote to this record — a person, an agent, an automation or the system itself. Read-only, stamped by the server on writes made through this API, and null on records last written any other way, so treat it as present-or-null rather than always populated. This is an actor id (a UUID), not a member id: look it up with `GET /actors/{id}`, whose `memberId` gives you the person to use against `/members`."},"updatedByActorId_in":{"type":"array","items":{"type":"string"},"description":"Filter \"updatedByActorId\" by in (operator filter)."},"archived":{"type":"boolean","description":"Whether this resource is archived."},"archivedAt":{"type":"string","description":"Timestamp when the task was archived. Server-stamped from the `archived` boolean — not directly writable."},"archivedAt_gt":{"type":"string","description":"Filter \"archivedAt\" by gt (operator filter)."},"archivedAt_gte":{"type":"string","description":"Filter \"archivedAt\" by gte (operator filter)."},"archivedAt_lt":{"type":"string","description":"Filter \"archivedAt\" by lt (operator filter)."},"archivedAt_lte":{"type":"string","description":"Filter \"archivedAt\" by lte (operator filter)."},"niceId":{"type":"number","description":"Per-project sequential counter assigned on insert. Compose with `project.niceId` for human-readable references (e.g. \"PROJ-42\"). Server-managed — read-only on the wire."},"niceId_in":{"type":"array","items":{"type":"number"},"description":"Filter \"niceId\" by in (operator filter)."},"projectId":{"type":"string","pattern":"^[0-9A-Za-z_!]{1,16}$","description":"Scope \"list\"/\"search\" to one project (project-scoped resources only). REQUIRED for \"search\": unlike \"list\", search has NO workspace-wide fallback — omitting it on \"search\" errors `project_not_specified`. \"list\" does NOT require it: omitting it on \"list\" falls back to a workspace-wide scan across every project you can access."},"projectId_in":{"type":"array","items":{"type":"string"},"description":"Filter \"projectId\" by in (operator filter)."},"statusId":{"type":"string","description":"The status column this task sits in. Null when the task has no status (e.g. a backlog item). Legal ids come from `GET /statuses`."},"statusId_in":{"type":"array","items":{"type":"string"},"description":"Filter \"statusId\" by in (operator filter)."},"listId":{"type":"string","description":"The list this task belongs to. Null when the task is not on a list. Legal ids come from `GET /lists`."},"listId_in":{"type":"array","items":{"type":"string"},"description":"Filter \"listId\" by in (operator filter)."},"parentTaskId":{"type":"string","description":"The parent task, when this task is a subtask. Null on top-level tasks. Subtask siblings are ranked by `order`."},"parentTaskId_in":{"type":"array","items":{"type":"string"},"description":"Filter \"parentTaskId\" by in (operator filter)."},"dependencyId":{"type":"string","description":"The single task this one waits on. Nifty models one predecessor per task, so this is an id and not an array. Null when the task has no predecessor."},"dependencyId_in":{"type":"array","items":{"type":"string"},"description":"Filter \"dependencyId\" by in (operator filter)."},"recurringRuleId":{"type":"string","description":"The recurrence rule that regenerates this task, for recurring tasks. Null otherwise. Legal ids come from `GET /recurring-rules`."},"recurringRuleId_in":{"type":"array","items":{"type":"string"},"description":"Filter \"recurringRuleId\" by in (operator filter)."},"startAt":{"type":"string","description":"When work on this task is scheduled to start."},"startAt_gt":{"type":"string","description":"Filter \"startAt\" by gt (operator filter)."},"startAt_gte":{"type":"string","description":"Filter \"startAt\" by gte (operator filter)."},"startAt_lt":{"type":"string","description":"Filter \"startAt\" by lt (operator filter)."},"startAt_lte":{"type":"string","description":"Filter \"startAt\" by lte (operator filter)."},"dueAt":{"type":"string","description":"When this task is due."},"dueAt_gt":{"type":"string","description":"Filter \"dueAt\" by gt (operator filter)."},"dueAt_gte":{"type":"string","description":"Filter \"dueAt\" by gte (operator filter)."},"dueAt_lt":{"type":"string","description":"Filter \"dueAt\" by lt (operator filter)."},"dueAt_lte":{"type":"string","description":"Filter \"dueAt\" by lte (operator filter)."},"completedAt":{"type":"string","description":"Timestamp when the task was marked complete. Server-stamped from the `completed` boolean — not directly writable."},"completedAt_gt":{"type":"string","description":"Filter \"completedAt\" by gt (operator filter)."},"completedAt_gte":{"type":"string","description":"Filter \"completedAt\" by gte (operator filter)."},"completedAt_lt":{"type":"string","description":"Filter \"completedAt\" by lt (operator filter)."},"completedAt_lte":{"type":"string","description":"Filter \"completedAt\" by lte (operator filter)."},"createdById":{"type":"string","description":"The team member who created this task. Server-set. Resolve it with `?expand=createdBy` or `GET /members/{id}`; pairs with `archivedById` and `completedById`. This is a member id — `createdByActorId` is a different identifier space and `GET /members/{id}` will not accept it."},"createdById_in":{"type":"array","items":{"type":"string"},"description":"Filter \"createdById\" by in (operator filter)."},"schema":{"type":"string","description":"The kind of `body`, copied from `body.schema` when the hand-off is created — filter on it to find the newest hand-off of your kind. Read-only."},"schema_in":{"type":"array","items":{"type":"string"},"description":"Filter \"schema\" by in (operator filter)."},"isShared":{"type":"boolean","description":"Shared layouts are visible to project members (or team-wide when `projectId` is null); personal ones are creator-only. Defaults to false."},"isDefault":{"type":"boolean","description":"The default layout for this (creator, type, project) — setting it unsets the previous default. Defaults to false."},"isPinned":{"type":"boolean","description":"Pinned layouts appear ahead of unpinned ones in the board’s view switcher. Defaults to false."},"id":{"type":"string","pattern":"^[0-9A-Za-z_!]{1,16}$","description":"Entity id — required for \"get\" and for instance-target read verb ops."},"limit":{"type":"integer","exclusiveMinimum":0,"description":"Items per page for \"list\"/\"search\" (default 50). Over the server cap it is silently clamped, not rejected — a short page does NOT mean the end; page until `nextCursor` is null."},"cursor":{"type":"string","description":"Opaque pagination cursor returned by a previous \"list\"/\"search\"."},"includeTotal":{"type":"boolean","description":"Opt-in. List `total` is `null` by default for performance (an extra COUNT over the full set). Set true to get `total` = the count of the FULL filtered + ACL-scoped result set (NOT the current page; ignores limit/cursor). The `total` key is always present: integer when requested, null when not. To ask only 'how many', pass includeTotal:true with limit:1 so you don't over-fetch rows. List-only (search always returns total)."},"query":{"type":"string","description":"Full-text query — REQUIRED for operation \"search\". The \"list\" operation has NO free-text search: it filters only by the resource's filterable fields. To search by text, use operation:\"search\" with \"query\" (not a \"search\" key on \"list\")."},"expand":{"type":"string","description":"Comma-separated relations to expand inline on the returned entity/entities. Nested paths allowed with dots (e.g. members.member) up to depth 4; nesting into a relation also requires that target's read scope. Available per resource — checklist: task/checklistItems/createdByActor/updatedByActor; checklistItem: checklist/createdByActor/updatedByActor; reminder: task/createdByActor/updatedByActor; task: project/status/list/parentTask/dependency/recurringRule/createdBy/archivedBy/completedBy/createdByActor/updatedByActor/assignees/labels/subscribers/hiddenFor/connectedTasks/subtasks/messages/checklists/files/customFields; taskHandoff: task/createdByActor/updatedByActor; tasksLayout: project/createdBy/createdByActor/updatedByActor. Unknown relations rejected. When expand is omitted, task \"get\" auto-includes customFields (pass expand explicitly to override)."},"sort":{"type":"string","description":"Sort \"list\"/\"search\" results by ONE column. Descending: prefix \"-\" (e.g. \"-updatedAt\") OR suffix \":desc\" (e.g. \"updatedAt:desc\"). Ascending: bare column (e.g. \"updatedAt\") OR suffix \":asc\". A single string — NOT separate sortBy/sortDir, and do NOT combine \"-\" with \":asc\"/\":desc\". An unlisted column is rejected with an error naming the valid ones. Sortable columns + default per resource — checklist: name/order/createdAt (default order); checklistItem: name/order/createdAt (default order); reminder: date/createdAt (default date); task: name/niceId/order/orderList/startAt/dueAt/createdAt/updatedAt/completedAt (default order with projectId, \"-updatedAt\" without); taskHandoff: createdAt (default -createdAt); tasksLayout: name/createdAt (default -createdAt). Those defaults are LIST-only: omit sort on \"list\" to get the default shown, but omit it on \"search\" and results come back ranked by text relevance instead. Pass sort explicitly (e.g. sort=order) to force a specific order on either."}},"required":["resource","operation"],"additionalProperties":true,"$schema":"http://json-schema.org/draft-07/schema#"},"annotations":{"title":"Find tasks, checklists and reminders","readOnlyHint":true,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false},"execution":{"taskSupport":"forbidden"},"_meta":{"requiredScopes":["checklist-items:read","checklists:read","reminders:read","task-handoffs:read","tasks-layouts:read","tasks:read"]}},{"name":"tasks_mutate","description":"Create or update tasks resources: checklist (A named, ordered container of checklist items on a task); checklistItem (A single completable line inside a checklist); reminder (A scheduled reminder anchored to a task); task (A unit of work in a project); taskHandoff (Working context for whoever picks a task up next — read the newest one of your kind before working a task, create one when you stop); tasksLayout (A saved task-board VIEW/LAYOUT for a user (per-user, optionally per-project): the board type + a `config` object (filters, grouping, sort, hidden columns, calendar range, etc.)). Pass {resource, operation} plus the resource's writable fields. Verb ops beyond create/update: createTree (Create a task with its subtasks and their checklists in one atomic call — no id-threading, no half-built trees); attachDocument (Attach an existing document to this task (id = the task, documentId = the document; both must already exist in the same workspace)); detachDocument (Detach a previously attached document from this task (id = the task, documentId = the document)). Also links/unlinks relations via addAssignees, addConnectedTasks, addHiddenFor, addLabels, addSubscribers, removeAssignees, removeConnectedTasks, removeHiddenFor, removeLabels, removeSubscribers — pass {resource, operation, id, ids}. Batch writes: bulkCreate, bulkUpdate — pass {resource, operation, items:[…], onError?}. Recovering a bulkCreate depends on whether you got a result envelope back. GOT ONE: trust it — it is index-aligned, rows marked ok are created, and you may send a NEW batch containing only the rows that never got created. NO ENVELOPE (timeout / lost response): resend the SAME items in the SAME order — that exact batch is deduplicated per row and replays what already landed instead of re-creating it. Do NOT re-split it or drop items first; that changes the dedup key and DOES create duplicates. bulkUpdate carries no dedup key, so it is safe to resend only because each item is an absolute field set — never resend one whose values are relative to the current value. Concurrency-safe updates: on a single-row update, pass expectedUpdatedAt = the updatedAt from the read your edit is based on; a stale_write (409) means the row changed since — re-read it, re-derive your change, and retry with the fresh updatedAt (never a timestamp echoed by the error). The guard requires at least one field change. Bulk items do not accept it: with onError absent, one item carrying it rejects the WHOLE batch client-side (0 HTTP calls); onError:'continue' ships the valid rows and returns per-item errors. Clearing a value: a field whose type includes \"null\" accepts an explicit JSON null to clear it (for an FK, to unlink it). Omitting a key leaves that field UNCHANGED — omission and null are different requests, so send only the keys you mean to change. Structure work with native fields, not prose: a tick-off step → a checklistItem; a step someone owns/schedules (its own assignee, due date, or status) → a subtask (a task with parentTaskId); a typed attribute recorded across tasks (budget / priority / a date) → a customField. Categorize a task by status (lifecycle stage), label (free-form, multiple per task), or customField (typed, one value per field). HARD RULE: before writing a bulleted or numbered list of actionable items inside a description, STOP — each line is a subtask or checklistItem per the mapping above; a work-list buried in prose is an anti-pattern (a description holds rationale/context/ evidence only). Mirror a source doc's hierarchy: a phase/milestone → a list or task, its steps → subtasks, a step's checks → checklistItems. \"Lean, not kitchen-sink\" governs FEATURES (modules, statuses, custom fields, views), NOT decomposition — splitting real work into subtasks/checklist is expected structure, not clutter. Link an existing document to a task via {resource: \"task\", operation: \"attachDocument\", id: <the task>, documentId} (detachDocument, same args, undoes it) — never paste a document body into a task.","inputSchema":{"type":"object","properties":{"resource":{"type":"string","enum":["checklist","checklistItem","reminder","task","taskHandoff","tasksLayout"],"description":"Which resource within this domain group to act on."},"operation":{"type":"string","enum":["addAssignees","addConnectedTasks","addHiddenFor","addLabels","addSubscribers","attachDocument","bulkCreate","bulkUpdate","create","createTree","detachDocument","removeAssignees","removeConnectedTasks","removeHiddenFor","removeLabels","removeSubscribers","update"],"description":"Which operation to run on the resource. Valid arguments depend on the (resource, operation) pair — unknown arguments are rejected with an error that names the accepted ones (the top-level schema is permissive because it forwards to a strict per-operation validator)."},"id":{"type":"string","pattern":"^[0-9A-Za-z_!]{1,16}$","description":"Entity id — for \"update\", the add{Relation}/remove{Relation} ops, and instance-target verb ops (id = the entity acted on)."},"ids":{"type":"array","items":{"type":"string","pattern":"^[0-9A-Za-z_!]{1,16}$","description":"related entity id"},"description":"Target ids for an add{Relation}/remove{Relation} op (1..500; auto-chunked)."},"items":{"type":"array","items":{"type":"object","properties":{},"additionalProperties":true},"minItems":1,"description":"bulkCreate/bulkUpdate only: array of per-item bodies, each the same shape as a single create/update (bulkUpdate items include id). Max 150."},"onError":{"type":"string","enum":["abort","continue"],"description":"bulk ops only: abort (default, stop at first failure) or continue (best-effort, per-item errors in the result)."},"expectedUpdatedAt":{"type":"string","format":"date-time","description":"update (single-row) only: concurrency guard — the `updatedAt` from the read your edit is based on. 409 stale_write if the row changed since; re-read, re-derive, retry with the fresh value. Requires ≥1 field change. Not accepted on bulk items."},"name":{"type":"string","description":"Checklist name — a group of steps on the task."},"order":{"type":["number","null"],"description":"Fractional rank of the checklist within its task. To reposition, send the midpoint of the two neighbours' ranks."},"taskId":{"type":"string","description":"Task this checklist belongs to. Required on create; immutable afterwards."},"completed":{"type":"boolean","description":"Whether the item is checked off. Defaults to false."},"checklistId":{"type":"string","description":"Checklist this item belongs to. Required on create; immutable afterwards."},"date":{"type":"string","description":"When the reminder fires, as an ISO 8601 instant (not a calendar date). Must be at least five minutes in the future. This is the only updatable field. Distinct from a task's dueAt: this fires a personal notification to the creator, not a deadline on the task itself."},"type":{"type":"string","description":"type — varies by resource; validated per (resource, operation) at call time"},"archived":{"type":"boolean","description":"Whether this resource is archived."},"description":{"type":["string","null"],"description":"Task description. Supports Markdown formatting. Embed refs: `<@memberId>` person, `<?taskId>` task, `<#projectId>` project, `<??taskGroupId>` status, `<^milestoneId>` list, `<~labelId>` label, `<$subteamId>` portfolio, `<!discussionId>` discussion. A project niceId + number (e.g. `PROJ-123`) auto-links that task; `:shortcode:` renders an emoji. Write mention/ref glyphs as plain literal text — wrapping one in backticks or other code formatting renders it as inert code instead of a resolved mention/link."},"projectId":{"type":["string","null"],"description":"Project this task belongs to. Required on create; PATCH-ing it moves the task — and its subtasks — into another project."},"statusId":{"type":["string","null"],"description":"The status column this task sits in. Null when the task has no status (e.g. a backlog item). Legal ids come from `GET /statuses`. Completion is the `completed` flag (a virtual 'Completed' column), not a status. Do not add a redundant 'Done'/'Completed' status."},"listId":{"type":["string","null"],"description":"The list this task belongs to. Null when the task is not on a list. Legal ids come from `GET /lists`."},"parentTaskId":{"type":["string","null"],"description":"The parent task, when this task is a subtask. Null on top-level tasks. Subtask siblings are ranked by `order`. Set to a parent task's id to create this as a SUBTASK of that task (rather than a top-level task)."},"dependencyId":{"type":["string","null"],"description":"The single task this one waits on. Nifty models one predecessor per task, so this is an id and not an array. Null when the task has no predecessor. Set to a prerequisite task's id (Gantt dependency): THIS task is the blocked one and by default cannot be completed until that blocker completes. Holds ONE blocker — a single FK cannot express a multi-part roll-up; work that is parts of one deliverable belongs as subtasks (parentTaskId): subtasks ARE the roll-up model. The write is rejected (400) when the blocker's dueAt is later than this task's dueAt. A related task that doesn't block this one (a follow-up or a side finding) is not a dependency: link it with addConnectedTasks."},"recurringRuleId":{"type":["string","null"],"description":"The recurrence rule that regenerates this task, for recurring tasks. Null otherwise. Legal ids come from `GET /recurring-rules`."},"startAt":{"type":["string","null"],"description":"When work on this task is scheduled to start."},"dueAt":{"type":["string","null"],"description":"When this task is due."},"remindAt":{"type":["string","null"],"description":"A single reminder time for this task. Recurring or multiple reminders are separate objects on the `reminder` resource (`GET /reminders?taskId[eq]=…`)."},"storyPoints":{"type":["number","null"],"description":"Story points used for agile estimation; client-supplied."},"orderList":{"type":["number","null"],"description":"Fractional rank of the task within its list. Same read/write shape and midpoint recipe as `order`."},"banner":{"type":["string","null"],"description":"Cover image URL rendered at the top of the task detail view."},"embedUrl":{"type":["string","null"],"description":"External URL embedded in the task body (e.g. Loom recording, Figma frame)."},"assignees":{"type":"array","items":{"type":"string","minLength":1},"maxItems":50,"description":"Team-member ids assigned to this task. On create, seeds the set. **On update, REPLACES the whole set — members you leave out are unassigned.** For additive changes use `POST`/`DELETE /tasks/{id}/assignees`. Read the current set with `?expand=assignees`."},"labels":{"type":"array","items":{"type":"string","minLength":1},"maxItems":50,"description":"Label ids attached to this task. On create, seeds the set. **On update, REPLACES the whole set — labels you leave out are detached.** For additive changes use `POST`/`DELETE /tasks/{id}/labels`. Read the current set with `?expand=labels`."},"subscribers":{"type":"array","items":{"type":"string","minLength":1},"maxItems":50,"description":"Team-member ids subscribed to this task (they receive notifications on changes). On create, seeds the set. **On update, REPLACES the whole set — members you leave out are unsubscribed.** For additive changes use `POST`/`DELETE /tasks/{id}/subscribers`. Read the current set with `?expand=subscribers`."},"customFields":{"type":"array","items":{"type":"object","properties":{"customFieldId":{"type":"string","minLength":1},"value":{"type":"string"}},"required":["customFieldId","value"],"additionalProperties":false},"maxItems":50,"description":"Custom-field values on this task. READ: `?expand=customFields` returns the values attached to this task. WRITE: include `customFields: [{ customFieldId, value }]` in the create/update body to set values — each one is checked against its field definition before the write commits (value is always a string; max 50 entries per request; `\"\"` clears). Field definitions live on the read-only `customField` resource."},"summary":{"type":"string","description":"One line a person reads on the task: what changed and what is next. Plain text, at most 500 characters, no markdown."},"body":{"type":"object","additionalProperties":{},"description":"Working context for whoever picks this task up next — a person or an agent. JSON at most 20,000 UTF-8 bytes, no string over 10,000 characters. `body.schema` names the kind: `nifty.checkpoint/1` is validated, any other `<vendor>.<kind>/<version>` is stored as-is (the `nifty.` namespace is reserved). Newest of a kind wins; never edit an old one. Put decisions WITH their reasons, checks WITH their result, and one concrete next action."},"config":{"type":["string","null"],"description":"View settings for this layout (filters, grouping, sort, hidden columns, calendar range, …). The shape is owned by the Nifty web app and changes with it — it is not part of any versioned contract."},"isAutoSave":{"type":"boolean","description":"When true, edits made on the board are written straight back to this layout instead of being discarded on navigation. Defaults to TRUE."},"isShared":{"type":"boolean","description":"Shared layouts are visible to project members (or team-wide when `projectId` is null); personal ones are creator-only. Defaults to false."},"isDefault":{"type":"boolean","description":"The default layout for this (creator, type, project) — setting it unsets the previous default. Defaults to false."},"isPinned":{"type":"boolean","description":"Pinned layouts appear ahead of unpinned ones in the board’s view switcher. Defaults to false."},"subtasks":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"},"assignees":{"type":"array","items":{"type":"string","minLength":1}},"labels":{"type":"array","items":{"type":"string","minLength":1}},"startAt":{"type":"string","format":"date-time"},"dueAt":{"type":"string","format":"date-time"},"completed":{"type":"boolean"},"statusId":{"type":"string","minLength":1},"customFields":{"type":"array","items":{"type":"object","properties":{"customFieldId":{"type":"string","minLength":1},"value":{"type":"string"}},"required":["customFieldId","value"],"additionalProperties":false},"maxItems":50},"checklists":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","minLength":1},"items":{"type":"array","items":{"type":"string","minLength":1},"minItems":1,"maxItems":100}},"required":["items"],"additionalProperties":false},"maxItems":20}},"required":["name"],"additionalProperties":false},"maxItems":100,"description":"subtasks — verb input for the operation named in `operation`; validated per (resource, operation) at call time."},"documentId":{"type":"string","minLength":1,"description":"documentId — verb input for the operation named in `operation`; validated per (resource, operation) at call time."}},"required":["resource","operation"],"additionalProperties":true,"$schema":"http://json-schema.org/draft-07/schema#"},"annotations":{"title":"Create or update tasks, checklists and reminders","readOnlyHint":false,"destructiveHint":true,"idempotentHint":false,"openWorldHint":false},"execution":{"taskSupport":"forbidden"},"_meta":{"requiredScopes":["checklist-items:write","checklists:write","reminders:write","task-handoffs:write","tasks-layouts:write","tasks:link","tasks:write"]}},{"name":"tasks_destroy","description":"Delete tasks resources: checklist (A named, ordered container of checklist items on a task); checklistItem (A single completable line inside a checklist); reminder (A scheduled reminder anchored to a task); task (A unit of work in a project); taskHandoff (Working context for whoever picks a task up next — read the newest one of your kind before working a task, create one when you stop); tasksLayout (A saved task-board VIEW/LAYOUT for a user (per-user, optionally per-project): the board type + a `config` object (filters, grouping, sort, hidden columns, calendar range, etc.)). Pass {resource, operation: \"delete\", id}. Batch delete: bulkDelete — pass {resource, operation: \"bulkDelete\", ids:[…], onError?}. Bulk deletes are NOT idempotent-safe on retry: deleting an already-deleted row reports resource_not_found, not success. If the response is lost, re-query the ids to see what actually remains and delete only those — do not blind-retry the batch, and do not read a resource_not_found on a lost-response retry as \"the delete failed\".","inputSchema":{"type":"object","properties":{"resource":{"type":"string","enum":["checklist","checklistItem","reminder","task","taskHandoff","tasksLayout"],"description":"Which resource within this domain group to act on."},"operation":{"type":"string","enum":["bulkDelete","delete"],"description":"Which operation to run on the resource. Valid arguments depend on the (resource, operation) pair — unknown arguments are rejected with an error that names the accepted ones (the top-level schema is permissive because it forwards to a strict per-operation validator)."},"id":{"type":"string","pattern":"^[0-9A-Za-z_!]{1,16}$","description":"Entity id to delete."},"ids":{"type":"array","items":{"type":"string","pattern":"^[0-9A-Za-z_!]{1,16}$"},"minItems":1,"description":"bulkDelete only: entity ids to delete. Max 150."},"onError":{"type":"string","enum":["abort","continue"],"description":"bulk ops only: abort (default, stop at first failure) or continue (best-effort, per-item errors in the result)."}},"required":["resource","operation"],"additionalProperties":true,"$schema":"http://json-schema.org/draft-07/schema#"},"annotations":{"title":"Delete tasks, checklists and reminders","readOnlyHint":false,"destructiveHint":true,"idempotentHint":true,"openWorldHint":false},"execution":{"taskSupport":"forbidden"},"_meta":{"requiredScopes":["checklist-items:delete","checklists:delete","reminders:delete","task-handoffs:delete","tasks-layouts:delete","tasks:delete"]}},{"name":"planning_query","description":"Read (list / get / search) planning resources: form (A team-scoped form (survey / intake / check-in) whose structure is a set of ordered fields, each with a type, a label, optional multiple-choice options, and optional conditional-logic rules); goal (A team objective with tracked progress (full CRUD)); goalGroup (A team-scoped grouping for organizing related goals (e.g., \"Q2 OKRs\", \"Engineering initiatives\")); list (A project-scoped task container); portfolio (A team-scoped grouping of projects); project (A top-level container in the Nifty workspace); projectFolder (A project folder — a grouping of projects inside a portfolio); status (A project-scoped status column for tasks (Kanban column, list stage)). Pass {resource, operation} plus the operation's args. For project-scoped resources here (list, status): \"search\" REQUIRES `projectId` (no workspace-wide fallback — omitting it errors `project_not_specified`); \"list\" does NOT require it (omitting it falls back to a workspace-wide scan).","inputSchema":{"type":"object","properties":{"resource":{"type":"string","enum":["form","goal","goalGroup","list","portfolio","project","projectFolder","status"],"description":"Which resource within this domain group to act on."},"operation":{"type":"string","enum":["get","list","search"],"description":"Which operation to run on the resource. Valid arguments depend on the (resource, operation) pair — unknown arguments are rejected with an error that names the accepted ones (the top-level schema is permissive because it forwards to a strict per-operation validator)."},"createdAt":{"type":"string","description":"Creation timestamp."},"createdAt_gt":{"type":"string","description":"Filter \"createdAt\" by gt (operator filter)."},"createdAt_gte":{"type":"string","description":"Filter \"createdAt\" by gte (operator filter)."},"createdAt_lt":{"type":"string","description":"Filter \"createdAt\" by lt (operator filter)."},"createdAt_lte":{"type":"string","description":"Filter \"createdAt\" by lte (operator filter)."},"purgeAt":{"type":"string","description":"When this resource will be permanently deleted — soft-delete time + 30-day retention; null while live."},"purgeAt_null":{"type":"string","description":"Filter \"purgeAt\" by null (operator filter)."},"purgeAt_notNull":{"type":"string","description":"Filter \"purgeAt\" by notNull (operator filter)."},"purgeAt_gt":{"type":"string","description":"Filter \"purgeAt\" by gt (operator filter)."},"purgeAt_gte":{"type":"string","description":"Filter \"purgeAt\" by gte (operator filter)."},"purgeAt_lt":{"type":"string","description":"Filter \"purgeAt\" by lt (operator filter)."},"purgeAt_lte":{"type":"string","description":"Filter \"purgeAt\" by lte (operator filter)."},"automation":{"type":"string","enum":["task","doc","message","checkin"],"description":"What Nifty does when the form is submitted: `task` creates a task, `doc` a document, `message` a chat message, `checkin` a check-in response. null for a plain form that just records answers. When it is `task`, both `targetStatusId` and `targetListId` are required in the same request."},"automation_in":{"type":"array","items":{"type":"string"},"description":"Filter \"automation\" by in (operator filter)."},"docAccessType":{"type":"string","enum":["public","limited","private"],"description":"Who can see the document a `doc` automation creates: `public` (the whole team), `limited` (invited members), `private` (the author only). Defaults to `public`."},"docAccessType_in":{"type":"array","items":{"type":"string"},"description":"Filter \"docAccessType\" by in (operator filter)."},"projectId":{"type":"string","pattern":"^[0-9A-Za-z_!]{1,16}$","description":"Scope \"list\"/\"search\" to one project (project-scoped resources only). REQUIRED for \"search\": unlike \"list\", search has NO workspace-wide fallback — omitting it on \"search\" errors `project_not_specified`. \"list\" does NOT require it: omitting it on \"list\" falls back to a workspace-wide scan across every project you can access."},"projectId_in":{"type":"array","items":{"type":"string"},"description":"Filter \"projectId\" by in (operator filter)."},"createdById":{"type":"string","description":"Team member who created this form — a member id, so resolve it with `GET /members/{id}`. Read-only, set from the caller. Null for creates that have no member behind them."},"createdById_in":{"type":"array","items":{"type":"string"},"description":"Filter \"createdById\" by in (operator filter)."},"name":{"type":"string","description":"Goal name"},"name_contains":{"type":"string","description":"Filter \"name\" by contains (operator filter)."},"name_startsWith":{"type":"string","description":"Filter \"name\" by startsWith (operator filter)."},"accessType":{"type":"string","description":"accessType — varies by resource; validated per (resource, operation) at call time"},"accessType_in":{"type":"array","items":{"type":"string"},"description":"Filter \"accessType\" by in (operator filter)."},"endAt":{"type":"string","description":"Target completion date (ISO 8601). Required on create; a goal always has a date it is aiming at."},"endAt_gt":{"type":"string","description":"Filter \"endAt\" by gt (operator filter)."},"endAt_gte":{"type":"string","description":"Filter \"endAt\" by gte (operator filter)."},"endAt_lt":{"type":"string","description":"Filter \"endAt\" by lt (operator filter)."},"endAt_lte":{"type":"string","description":"Filter \"endAt\" by lte (operator filter)."},"ownerId":{"type":"string","description":"The team member who owns this goal — for a `private` goal, the only member who can see it. Defaults to the caller on create; send another team member id to hand it over."},"ownerId_in":{"type":"array","items":{"type":"string"},"description":"Filter \"ownerId\" by in (operator filter)."},"goalGroupId":{"type":"string","description":"Goal group this goal is filed under (nullable — ON DELETE SET NULL). Writable: an id files the goal under that group; explicit `null` unlinks; omitted leaves it unchanged."},"goalGroupId_in":{"type":"array","items":{"type":"string"},"description":"Filter \"goalGroupId\" by in (operator filter)."},"updatedAt":{"type":"string","description":"Last update timestamp."},"updatedAt_gt":{"type":"string","description":"Filter \"updatedAt\" by gt (operator filter)."},"updatedAt_gte":{"type":"string","description":"Filter \"updatedAt\" by gte (operator filter)."},"updatedAt_lt":{"type":"string","description":"Filter \"updatedAt\" by lt (operator filter)."},"updatedAt_lte":{"type":"string","description":"Filter \"updatedAt\" by lte (operator filter)."},"archived":{"type":"boolean","description":"Whether this list is archived (hidden from the active timeline but preserved). Regular field, NOT a soft-delete."},"start":{"type":"string","description":"Start date (ISO 8601). **Required together with `end` whenever `isMilestone` is true** — including when flipping it true with a PATCH. `end` must be at least 2 days after `start`; use a task with a due date for single-day work. Forced to null when `isMilestone` is false."},"start_gt":{"type":"string","description":"Filter \"start\" by gt (operator filter)."},"start_gte":{"type":"string","description":"Filter \"start\" by gte (operator filter)."},"start_lt":{"type":"string","description":"Filter \"start\" by lt (operator filter)."},"start_lte":{"type":"string","description":"Filter \"start\" by lte (operator filter)."},"end":{"type":"string","description":"End date (ISO 8601). **Required together with `start` whenever `isMilestone` is true** — including when flipping it true with a PATCH. Must be at least 2 days after `start`; use a task with a due date for single-day work. Forced to null when `isMilestone` is false."},"end_gt":{"type":"string","description":"Filter \"end\" by gt (operator filter)."},"end_gte":{"type":"string","description":"Filter \"end\" by gte (operator filter)."},"end_lt":{"type":"string","description":"Filter \"end\" by lt (operator filter)."},"end_lte":{"type":"string","description":"Filter \"end\" by lte (operator filter)."},"isMilestone":{"type":"boolean","description":"false (the default) = a flat list of tasks. true = a date-spanning milestone shown on the roadmap; it requires `start` and `end` at least 2 days apart."},"dependencyId":{"type":"string","description":"Id of the list that must finish before this one starts (timeline dependency). The target must belong to the same project, otherwise the request is rejected with 400."},"dependencyId_in":{"type":"array","items":{"type":"string"},"description":"Filter \"dependencyId\" by in (operator filter)."},"ruleId":{"type":"string","description":"FK to the recurring rule governing when this list recurs"},"ruleId_in":{"type":"array","items":{"type":"string"},"description":"Filter \"ruleId\" by in (operator filter)."},"statusId":{"type":"string","description":"When set, this list also appears as a column on the project board, backed by the given status. Cannot be changed after creation."},"statusId_in":{"type":"array","items":{"type":"string"},"description":"Filter \"statusId\" by in (operator filter)."},"isGeneral":{"type":"string","description":"True for the team's General portfolio. Every team has exactly one: projects created without a `portfolioId` land there, and it cannot be deleted."},"order":{"type":"number","description":"Sort order of this portfolio within the team. Auto-assigned on create; send a new value to reorder."},"order_gt":{"type":"number","description":"Filter \"order\" by gt (operator filter)."},"order_gte":{"type":"number","description":"Filter \"order\" by gte (operator filter)."},"order_lt":{"type":"number","description":"Filter \"order\" by lt (operator filter)."},"order_lte":{"type":"number","description":"Filter \"order\" by lte (operator filter)."},"portfolioId":{"type":"string","description":"The portfolio this project is grouped under. Defaults to the team's General portfolio when omitted on create. Immutable after create."},"portfolioId_in":{"type":"array","items":{"type":"string"},"description":"Filter \"portfolioId\" by in (operator filter)."},"id":{"type":"string","pattern":"^[0-9A-Za-z_!]{1,16}$","description":"Entity id — required for \"get\" and for instance-target read verb ops."},"limit":{"type":"integer","exclusiveMinimum":0,"description":"Items per page for \"list\"/\"search\" (default 50). Over the server cap it is silently clamped, not rejected — a short page does NOT mean the end; page until `nextCursor` is null."},"cursor":{"type":"string","description":"Opaque pagination cursor returned by a previous \"list\"/\"search\"."},"includeTotal":{"type":"boolean","description":"Opt-in. List `total` is `null` by default for performance (an extra COUNT over the full set). Set true to get `total` = the count of the FULL filtered + ACL-scoped result set (NOT the current page; ignores limit/cursor). The `total` key is always present: integer when requested, null when not. To ask only 'how many', pass includeTotal:true with limit:1 so you don't over-fetch rows. List-only (search always returns total)."},"query":{"type":"string","description":"Full-text query — REQUIRED for operation \"search\". The \"list\" operation has NO free-text search: it filters only by the resource's filterable fields. To search by text, use operation:\"search\" with \"query\" (not a \"search\" key on \"list\")."},"expand":{"type":"string","description":"Comma-separated relations to expand inline on the returned entity/entities. Nested paths allowed with dots (e.g. members.member) up to depth 4; nesting into a relation also requires that target's read scope. Available per resource — form: project/createdByActor/updatedByActor/deletedBy; goal: owner/goalGroup/members/tasks/lists/createdByActor/updatedByActor; goalGroup: goals/createdByActor; list: createdByActor/updatedByActor/project/dependency/recurringRule/status/assignees/hiddenFor/goals/customFields/tasks; portfolio: owner/createdByActor/updatedByActor/deletedBy/members; project: createdByActor/updatedByActor/deletedBy/members/portfolio/discussionChat/customFields; projectFolder: portfolio/projects/createdByActor/updatedByActor; status: createdByActor/project/assignees/hiddenFor/list/tasks/recurringRules. Unknown relations rejected."},"sort":{"type":"string","description":"Sort \"list\"/\"search\" results by ONE column. Descending: prefix \"-\" (e.g. \"-updatedAt\") OR suffix \":desc\" (e.g. \"updatedAt:desc\"). Ascending: bare column (e.g. \"updatedAt\") OR suffix \":asc\". A single string — NOT separate sortBy/sortDir, and do NOT combine \"-\" with \":asc\"/\":desc\". An unlisted column is rejected with an error naming the valid ones. Sortable columns + default per resource — form: createdAt (default createdAt); goal: name/endAt/createdAt (default createdAt); goalGroup: name/createdAt (default createdAt); list: name/start/end/order/createdAt/updatedAt (default order with projectId, \"-updatedAt\" without); portfolio: name/order/createdAt (default order); project: name/createdAt/updatedAt/order (default order); projectFolder: name/order/createdAt (default order); status: name/order/createdAt/updatedAt (default order with projectId, \"-updatedAt\" without). Those defaults are LIST-only: omit sort on \"list\" to get the default shown, but omit it on \"search\" and results come back ranked by text relevance instead. Pass sort explicitly (e.g. sort=order) to force a specific order on either."}},"required":["resource","operation"],"additionalProperties":true,"$schema":"http://json-schema.org/draft-07/schema#"},"annotations":{"title":"Find projects, portfolios, milestones and goals","readOnlyHint":true,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false},"execution":{"taskSupport":"forbidden"},"_meta":{"requiredScopes":["forms:read","goal-groups:read","goals:read","lists:read","portfolios:read","project-folders:read","projects:read","statuses:read"]}},{"name":"planning_mutate","description":"Create or update planning resources: form (A team-scoped form (survey / intake / check-in) whose structure is a set of ordered fields, each with a type, a label, optional multiple-choice options, and optional conditional-logic rules); goal (A team objective with tracked progress (full CRUD)); goalGroup (A team-scoped grouping for organizing related goals (e.g., \"Q2 OKRs\", \"Engineering initiatives\")); list (A project-scoped task container); portfolio (A team-scoped grouping of projects); project (A top-level container in the Nifty workspace); projectFolder (A project folder — a grouping of projects inside a portfolio); status (A project-scoped status column for tasks (Kanban column, list stage)). Pass {resource, operation} plus the resource's writable fields. Verb ops beyond create/update: addMember (Add an existing team member to this project). Also links/unlinks relations via addAssignees, addGoals, addHiddenFor, addMembers, removeAssignees, removeGoals, removeHiddenFor, removeMembers — pass {resource, operation, id, ids}. Batch writes: bulkCreate, bulkUpdate — pass {resource, operation, items:[…], onError?}. Recovering a bulkCreate depends on whether you got a result envelope back. GOT ONE: trust it — it is index-aligned, rows marked ok are created, and you may send a NEW batch containing only the rows that never got created. NO ENVELOPE (timeout / lost response): resend the SAME items in the SAME order — that exact batch is deduplicated per row and replays what already landed instead of re-creating it. Do NOT re-split it or drop items first; that changes the dedup key and DOES create duplicates. bulkUpdate carries no dedup key, so it is safe to resend only because each item is an absolute field set — never resend one whose values are relative to the current value. Concurrency-safe updates: on a single-row update, pass expectedUpdatedAt = the updatedAt from the read your edit is based on; a stale_write (409) means the row changed since — re-read it, re-derive your change, and retry with the fresh updatedAt (never a timestamp echoed by the error). The guard requires at least one field change. Bulk items do not accept it: with onError absent, one item carrying it rejects the WHOLE batch client-side (0 HTTP calls); onError:'continue' ships the valid rows and returns per-item errors. Clearing a value: a field whose type includes \"null\" accepts an explicit JSON null to clear it (for an FK, to unlink it). Omitting a key leaves that field UNCHANGED — omission and null are different requests, so send only the keys you mean to change. Add a list to an existing project for new work; create a whole project only for a genuinely new initiative. A list groups tasks (its Roadmap visibility is automatic); a status is a task's lifecycle stage; a goal is a cross-project, org-level objective (not project-level date grouping — use a list for that). Configure what you create to the user's stated purpose rather than taking kitchen-sink defaults: on project create pass only the `modules` that fit — OMITTING them enables ALL 8 feature tabs, which clutters a focused project (content calendar → [tasks, calendar, files]; bug tracker → [tasks]; knowledge base → [documents, files]). A lean setup the user can extend beats a cluttered one.","inputSchema":{"type":"object","properties":{"resource":{"type":"string","enum":["form","goal","goalGroup","list","portfolio","project","projectFolder","status"],"description":"Which resource within this domain group to act on."},"operation":{"type":"string","enum":["addAssignees","addGoals","addHiddenFor","addMember","addMembers","bulkCreate","bulkUpdate","create","removeAssignees","removeGoals","removeHiddenFor","removeMembers","update"],"description":"Which operation to run on the resource. Valid arguments depend on the (resource, operation) pair — unknown arguments are rejected with an error that names the accepted ones (the top-level schema is permissive because it forwards to a strict per-operation validator)."},"id":{"type":"string","pattern":"^[0-9A-Za-z_!]{1,16}$","description":"Entity id — for \"update\", the add{Relation}/remove{Relation} ops, and instance-target verb ops (id = the entity acted on)."},"ids":{"type":"array","items":{"type":"string","pattern":"^[0-9A-Za-z_!]{1,16}$","description":"related entity id"},"description":"Target ids for an add{Relation}/remove{Relation} op (1..500; auto-chunked)."},"items":{"type":"array","items":{"type":"object","properties":{},"additionalProperties":true},"minItems":1,"description":"bulkCreate/bulkUpdate only: array of per-item bodies, each the same shape as a single create/update (bulkUpdate items include id). Max 150."},"onError":{"type":"string","enum":["abort","continue"],"description":"bulk ops only: abort (default, stop at first failure) or continue (best-effort, per-item errors in the result)."},"expectedUpdatedAt":{"type":"string","format":"date-time","description":"update (single-row) only: concurrency guard — the `updatedAt` from the read your edit is based on. 409 stale_write if the row changed since; re-read, re-derive, retry with the fresh value. Requires ≥1 field change. Not accepted on bulk items."},"automation":{"type":"string","enum":["task","doc","message","checkin"],"description":"What Nifty does when the form is submitted: `task` creates a task, `doc` a document, `message` a chat message, `checkin` a check-in response. null for a plain form that just records answers. When it is `task`, both `targetStatusId` and `targetListId` are required in the same request."},"targetStatusId":{"type":["string","null"],"description":"The board column a `task` automation files its new task into. Required whenever `automation` is `task`."},"targetListId":{"type":["string","null"],"description":"The list a `task` automation files its new task into. Required whenever `automation` is `task`."},"meta":{"type":"object","additionalProperties":{},"description":"The form's display metadata. `title` is the form's name, shown to respondents (also surfaced read-only as `name`); `description` is the blurb under it."},"success":{"type":"object","additionalProperties":{},"description":"What a respondent sees after submitting: a heading, body text, an optional image, an optional redirect, and an optional auto-reply email. Stored verbatim, so its keys are snake_case unlike the rest of this API."},"multiSubmit":{"type":"boolean","description":"Whether one respondent may submit the form more than once. Defaults to false."},"docAccessType":{"type":"string","enum":["public","limited","private"],"description":"Who can see the document a `doc` automation creates: `public` (the whole team), `limited` (invited members), `private` (the author only). Defaults to `public`."},"projectId":{"type":["string","null"],"description":"An optional project to associate this form with. It does not affect who can see the form — forms are visible across the whole workspace."},"fields":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Include an existing field id to keep that field. A field you omit from the array is deleted."},"type":{"type":"string","enum":["text","textarea","dropdown","checkboxes","number","date","file","email"],"description":"The kind of question. `checkboxes` and `dropdown` require a non-empty `options[]`."},"label":{"type":"string","description":"The question text shown to respondents."},"required":{"type":"boolean","description":"Whether the respondent must answer. Defaults to false on a newly added field."},"useAsTaskName":{"type":["boolean","null"],"description":"When true, this field's answer becomes the name of the item the form's automation creates. Set it on at most one field."},"clientKey":{"type":["string","null"],"description":"An opaque key of your own choosing, stable across edits. Conditions reference fields by this key (`condition.field_fe_id`), so give a field one before pointing a condition at it."},"description":{"type":["string","null"],"description":"Helper text shown under the question."},"randomize":{"type":["boolean","null"],"description":"Shuffle this field's choices for each respondent."},"withOther":{"type":["boolean","null"],"description":"Append a free-text \"Other\" choice to this field's options."},"taskField":{"anyOf":[{"type":"object","properties":{"id":{"type":"string","description":"Id of the target — a custom-field id for a custom-field target."},"type":{"type":"string","description":"Which task property the answer fills in."}},"additionalProperties":true},{"type":"null"}]},"condition":{"anyOf":[{"type":"object","properties":{"field_fe_id":{"type":"string","description":"The `clientKey` of the field whose answer is tested. Note the snake_case key: this object is stored verbatim as JSON."},"type":{"type":"string","enum":["eq","neq","contains","ncontains","gt","lt","selected","nselected","populated","npopulated"],"description":"How the other field's answer is compared to `value`."},"value":{"type":"string","description":"The value compared against. Ignored by `populated` and `npopulated`."}},"additionalProperties":true},{"type":"null"}]},"statusCondition":{"anyOf":[{"type":"object","properties":{"resource_type":{"type":"string","description":"What the two ids below point at."},"match_resource_id":{"type":"string","description":"Used when the answer matches the condition."},"no_match_resource_id":{"type":"string","description":"Used when the answer does not match."},"value":{"type":"string","description":"The value compared against."},"type":{"$ref":"#/properties/fields/items/properties/condition/anyOf/0/properties/type","description":"How the field's answer is compared to `value`."}},"additionalProperties":true},{"type":"null"}]},"listCondition":{"anyOf":[{"$ref":"#/properties/fields/items/properties/statusCondition/anyOf/0"},{"type":"null"}]},"options":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"value":{"type":"string","description":"A single selectable choice value."}},"required":["value"],"additionalProperties":false},"maxItems":100,"description":"The selectable choices. Required and non-empty for `checkboxes` and `dropdown`; ignored by every other type."}},"required":["type","label"],"additionalProperties":false},"maxItems":200,"description":"The form's questions, in the order respondents see them. **This is a full replacement**: any existing field missing from the array is deleted, along with its options and any answers already submitted to it — include each field's `id` to keep it. Omit the key entirely to leave the questions untouched. Cannot be an empty array (a form keeps at least one question), and cannot exceed 200 fields."},"name":{"type":"string","description":"Goal name"},"description":{"type":["string","null"],"description":"Optional longer description. Supports Markdown. Embed refs: `<@memberId>` person, `<?taskId>` task, `<#projectId>` project, `<??taskGroupId>` status, `<^milestoneId>` list, `<~labelId>` label, `<$subteamId>` portfolio, `<!discussionId>` discussion. A project niceId + number (e.g. `PROJ-123`) auto-links that task; `:shortcode:` renders an emoji. Write mention/ref glyphs as plain literal text — wrapping one in backticks or other code formatting renders it as inert code instead of a resolved mention/link."},"accessType":{"type":"string","description":"accessType — varies by resource; validated per (resource, operation) at call time"},"endAt":{"type":"string","description":"Target completion date (ISO 8601). Required on create; a goal always has a date it is aiming at."},"goalSourceType":{"type":"string","enum":["customField","manual","label","assigneeLabel","assignee","custom_field","assignee_label"],"description":"What feeds the goal's task set: `customField`, `manual`, `label`, `assigneeLabel`, `assignee`."},"goalSourceValue":{"type":["string","null"],"description":"The id `goalSourceType` points at: for `label` a label id; for `customField` a custom-field id; for `assignee` and `assigneeLabel` a team-member id. Not used when `goalSourceType` is `manual` (send the task ids in `tasks` instead)."},"goalSourceOption":{"type":["string","null"],"description":"Narrows `goalSourceValue` when `goalSourceType` is `custom_field`: the id of the dropdown option to match. Ignored for every other source type."},"trackMetricType":{"type":"string","enum":["taskCount","trackedTime","customFieldValue","storyPoints","task_count","tracked_time","custom_field_value","story_points"],"description":"How progress is measured: `taskCount`, `trackedTime`, `customFieldValue`, `storyPoints`."},"trackMetricValue":{"type":["string","null"],"description":"The id `trackMetricType` points at — for `custom_field_value`, the custom-field id whose values are summed. Not used by the other metric types."},"targetType":{"type":"string","enum":["dynamic","static"],"description":"Target mode: `dynamic` (the target tracks the live total of the goal task set) or `static` (a fixed `targetValue`)."},"targetValue":{"type":"number","description":"The fixed numeric target. Used only when `targetType` is `static`; when `targetType` is `dynamic` it is recomputed by the server from the goal task set and anything you send is overwritten."},"ownerId":{"type":["string","null"],"description":"The team member who owns this goal — for a `private` goal, the only member who can see it. Defaults to the caller on create; send another team member id to hand it over."},"goalGroupId":{"type":["string","null"],"description":"Goal group this goal is filed under (nullable — ON DELETE SET NULL). Writable: an id files the goal under that group; explicit `null` unlinks; omitted leaves it unchanged."},"members":{"type":"array","items":{"type":"string","minLength":1},"description":"Team members this goal is shared with (this is who `accessType: limited` means). Set when the goal is created; ignored on update. Read them back with `?expand=members`."},"tasks":{"type":"array","items":{"type":"string","minLength":1},"description":"Tasks linked to this goal, for goals whose `goalSourceType` is `manual`. Set when the goal is created; ignored on update. Read them back with `?expand=tasks`."},"lists":{"type":"array","items":{"type":"string","minLength":1},"description":"Lists whose tasks feed this goal. Set when the goal is created; ignored on update. Read them back with `?expand=lists`."},"archived":{"type":"boolean","description":"Whether this list is archived (hidden from the active timeline but preserved). Regular field, NOT a soft-delete."},"start":{"type":["string","null"],"description":"Start date (ISO 8601). **Required together with `end` whenever `isMilestone` is true** — including when flipping it true with a PATCH. `end` must be at least 2 days after `start`; use a task with a due date for single-day work. Forced to null when `isMilestone` is false."},"end":{"type":["string","null"],"description":"End date (ISO 8601). **Required together with `start` whenever `isMilestone` is true** — including when flipping it true with a PATCH. Must be at least 2 days after `start`; use a task with a due date for single-day work. Forced to null when `isMilestone` is false."},"order":{"type":["number","null"],"description":"Sort order within the project timeline. Server-defaulted to MAX(order)+100 when omitted on create."},"color":{"type":["string","null"],"description":"Hex color, `#RGB` or `#RRGGBB`. Stored lowercase and expanded to 6 digits, so `#0A9` reads back as `#00aa99`. Omitted on create → the server assigns a random palette color, so a list always has one."},"isMilestone":{"type":"boolean","description":"false (the default) = a flat list of tasks. true = a date-spanning milestone shown on the roadmap; it requires `start` and `end` at least 2 days apart."},"dependencyId":{"type":["string","null"],"description":"Id of the list that must finish before this one starts (timeline dependency). The target must belong to the same project, otherwise the request is rejected with 400. Direction: the record carrying dependencyId is the BLOCKED one — the referenced record is its prerequisite. Ordering is advisory here (completion is not enforced); what IS enforced: the dependency must belong to the same project (milestone_dependency_wrong_project)."},"statusId":{"type":["string","null"],"description":"When set, this list also appears as a column on the project board, backed by the given status. Cannot be changed after creation."},"assignees":{"type":"array","items":{"type":"string","minLength":1},"description":"Team members assigned to this list. Settable on create and update (whole-set replace), read back with `?expand=assignees`, or edited one at a time via `POST`/`DELETE /lists/{id}/assignees`."},"hiddenFor":{"type":"array","items":{"type":"string","minLength":1},"description":"Roles this list is hidden from. Settable on create and update (whole-set replace), read back with `?expand=hiddenFor`, or edited one at a time via `POST /lists/{id}/hidden-for` and `POST /lists/{id}/hidden-for/remove`."},"goals":{"type":"array","items":{"type":"string","minLength":1},"description":"Goals this list contributes toward. Settable on create and update (whole-set replace), read back with `?expand=goals`, or edited one at a time via `POST`/`DELETE /lists/{id}/goals`."},"customFields":{"type":"array","items":{"type":"object","properties":{"customFieldId":{"type":"string","minLength":1},"value":{"type":"string"}},"required":["customFieldId","value"],"additionalProperties":false},"maxItems":50,"description":"Set custom-field values on this list. Each entry is `{ customFieldId, value }` and `value` is always a string — send `\"\"` to clear one. Max 50 entries per request. Read them back with `?expand=customFields`."},"icon":{"type":["string","null"],"description":"Icon image URL for the portfolio."},"initials":{"type":"string","description":"Up to 3 characters shown when the portfolio has no icon. Derived from `name` when omitted on create."},"secondaryColor":{"type":"string","description":"Secondary brand colour as a 6-digit hex value. Derived from `color`; values sent by a client are ignored."},"portfolioId":{"type":["string","null"],"description":"The portfolio this project is grouped under. Defaults to the team's General portfolio when omitted on create. Immutable after create."},"modules":{"type":"array","items":{},"description":"Feature tabs the project exposes. SELECT only the ones this project needs — OMITTING the field enables ALL 8, which clutters a focused project. Options: `tasks` (the task board), `milestones` (dated roadmap of lists), `calendar` (calendar of dated items), `documents` (collaborative docs), `files` (file storage), `discussion` (threaded project discussions), `forms` (intake/survey forms), `home` (project overview). e.g. a content calendar → [tasks, calendar, files]; a bug tracker → [tasks]; a knowledge base → [documents, files]. Set once at create; immutable afterwards."},"defaultTasksView":{"type":"string","enum":["table","calendar","swimlane","kanban","timeline","report"],"description":"Default task board view: `table`, `calendar`, `swimlane`, `kanban`, `timeline` or `report`."},"hiddenListColumns":{"type":"array","items":{},"description":"Columns hidden on the list view. Each entry is either a built-in column key (`name`, `assignee`, `dueDate`, `status`, `list`, `labels`, `estimate`) or the id of a custom field attached to the project."},"listColumnsOrder":{"type":"array","items":{},"description":"List-view columns in display order, using the same vocabulary as `hiddenListColumns` — built-in column keys and/or attached custom-field ids."},"customFieldsOrder":{"type":"array","items":{},"description":"Project-level custom-field ids in display order."},"taskCustomFieldsOrder":{"type":"array","items":{},"description":"Task-level custom-field ids in display order."},"listCustomFieldsOrder":{"type":"array","items":{},"description":"List-level custom-field ids in display order."},"autoMilestones":{"type":"boolean","description":"When true, monthly lists are created automatically for this project. Applied at create time."},"hideUsersFromGuests":{"type":"boolean","description":"Hide member list from guest role"},"subtaskStatusEnabled":{"type":"boolean","description":"Enable per-subtask status tracking"},"queryAllFiles":{"type":"boolean","description":"Query files across all tasks"},"projectFolderId":{"type":["string","null"],"description":"The project folder this project is filed under, within its portfolio. null = unfiled."},"memberId":{"type":"string","minLength":1,"description":"memberId — verb input for the operation named in `operation`; validated per (resource, operation) at call time."}},"required":["resource","operation"],"additionalProperties":true,"$schema":"http://json-schema.org/draft-07/schema#"},"annotations":{"title":"Create or update projects, portfolios, milestones and goals","readOnlyHint":false,"destructiveHint":true,"idempotentHint":false,"openWorldHint":false},"execution":{"taskSupport":"forbidden"},"_meta":{"requiredScopes":["forms:write","goal-groups:write","goals:write","lists:link","lists:write","portfolios:link","portfolios:write","project-folders:write","projects:write","statuses:link","statuses:write"]}},{"name":"planning_destroy","description":"Delete planning resources: form (A team-scoped form (survey / intake / check-in) whose structure is a set of ordered fields, each with a type, a label, optional multiple-choice options, and optional conditional-logic rules); goal (A team objective with tracked progress (full CRUD)); goalGroup (A team-scoped grouping for organizing related goals (e.g., \"Q2 OKRs\", \"Engineering initiatives\")); list (A project-scoped task container); portfolio (A team-scoped grouping of projects); project (A top-level container in the Nifty workspace); projectFolder (A project folder — a grouping of projects inside a portfolio); status (A project-scoped status column for tasks (Kanban column, list stage)). Pass {resource, operation: \"delete\", id}. Batch delete: bulkDelete — pass {resource, operation: \"bulkDelete\", ids:[…], onError?}. Bulk deletes are NOT idempotent-safe on retry: deleting an already-deleted row reports resource_not_found, not success. If the response is lost, re-query the ids to see what actually remains and delete only those — do not blind-retry the batch, and do not read a resource_not_found on a lost-response retry as \"the delete failed\".","inputSchema":{"type":"object","properties":{"resource":{"type":"string","enum":["form","goal","goalGroup","list","portfolio","project","projectFolder","status"],"description":"Which resource within this domain group to act on."},"operation":{"type":"string","enum":["bulkDelete","delete"],"description":"Which operation to run on the resource. Valid arguments depend on the (resource, operation) pair — unknown arguments are rejected with an error that names the accepted ones (the top-level schema is permissive because it forwards to a strict per-operation validator)."},"id":{"type":"string","pattern":"^[0-9A-Za-z_!]{1,16}$","description":"Entity id to delete."},"ids":{"type":"array","items":{"type":"string","pattern":"^[0-9A-Za-z_!]{1,16}$"},"minItems":1,"description":"bulkDelete only: entity ids to delete. Max 150."},"onError":{"type":"string","enum":["abort","continue"],"description":"bulk ops only: abort (default, stop at first failure) or continue (best-effort, per-item errors in the result)."}},"required":["resource","operation"],"additionalProperties":true,"$schema":"http://json-schema.org/draft-07/schema#"},"annotations":{"title":"Delete projects, portfolios, milestones and goals","readOnlyHint":false,"destructiveHint":true,"idempotentHint":true,"openWorldHint":false},"execution":{"taskSupport":"forbidden"},"_meta":{"requiredScopes":["forms:delete","goal-groups:delete","goals:delete","lists:delete","portfolios:delete","project-folders:delete","projects:delete","statuses:delete"]}},{"name":"people_query","description":"Read (list / get / search) people resources: member (A person inside a team workspace); presenceStatus (A reusable team-scoped template for member statuses — e.g., \"In a meeting\", \"Focus time\", \"Out of office\"). Pass {resource, operation} plus the operation's args.","inputSchema":{"type":"object","properties":{"resource":{"type":"string","enum":["member","presenceStatus"],"description":"Which resource within this domain group to act on."},"operation":{"type":"string","enum":["get","list","search"],"description":"Which operation to run on the resource. Valid arguments depend on the (resource, operation) pair — unknown arguments are rejected with an error that names the accepted ones (the top-level schema is permissive because it forwards to a strict per-operation validator)."},"name":{"type":"string","description":"Member's display name"},"name_contains":{"type":"string","description":"Filter \"name\" by contains (operator filter)."},"name_startsWith":{"type":"string","description":"Filter \"name\" by startsWith (operator filter)."},"email":{"type":"string","description":"Member's email address."},"email_contains":{"type":"string","description":"Filter \"email\" by contains (operator filter)."},"online":{"type":"boolean","description":"Whether the member was connected at the moment this response was built. Point-in-time only; it is not pushed to you."},"pending":{"type":"boolean","description":"True while the member has been invited but has not accepted yet. An active member has `pending: false` and `removed: false`."},"appearsOnline":{"type":"boolean","description":"Whether the member is shown as online to their team while this status is active. This is an intent you set, not a live presence reading."},"isDefault":{"type":"boolean","description":"True for the templates Nifty seeds into every workspace. Server-managed: you cannot promote or demote a template through the API."},"id":{"type":"string","pattern":"^[0-9A-Za-z_!]{1,16}$","description":"Entity id — required for \"get\" and for instance-target read verb ops."},"limit":{"type":"integer","exclusiveMinimum":0,"description":"Items per page for \"list\"/\"search\" (default 50). Over the server cap it is silently clamped, not rejected — a short page does NOT mean the end; page until `nextCursor` is null."},"cursor":{"type":"string","description":"Opaque pagination cursor returned by a previous \"list\"/\"search\"."},"includeTotal":{"type":"boolean","description":"Opt-in. List `total` is `null` by default for performance (an extra COUNT over the full set). Set true to get `total` = the count of the FULL filtered + ACL-scoped result set (NOT the current page; ignores limit/cursor). The `total` key is always present: integer when requested, null when not. To ask only 'how many', pass includeTotal:true with limit:1 so you don't over-fetch rows. List-only (search always returns total)."},"query":{"type":"string","description":"Full-text query — REQUIRED for operation \"search\". The \"list\" operation has NO free-text search: it filters only by the resource's filterable fields. To search by text, use operation:\"search\" with \"query\" (not a \"search\" key on \"list\")."},"expand":{"type":"string","description":"Comma-separated relations to expand inline on the returned entity/entities. Nested paths allowed with dots (e.g. members.member) up to depth 4; nesting into a relation also requires that target's read scope. Available per resource — member: customFields; presenceStatus: createdByActor. Unknown relations rejected."},"sort":{"type":"string","description":"Sort \"list\"/\"search\" results by ONE column. Descending: prefix \"-\" (e.g. \"-updatedAt\") OR suffix \":desc\" (e.g. \"updatedAt:desc\"). Ascending: bare column (e.g. \"updatedAt\") OR suffix \":asc\". A single string — NOT separate sortBy/sortDir, and do NOT combine \"-\" with \":asc\"/\":desc\". An unlisted column is rejected with an error naming the valid ones. Sortable columns + default per resource — member: name/email (default name); presenceStatus: name/createdAt (default createdAt). Those defaults are LIST-only: omit sort on \"list\" to get the default shown, but omit it on \"search\" and results come back ranked by text relevance instead. Pass sort explicitly (e.g. sort=order) to force a specific order on either."}},"required":["resource","operation"],"additionalProperties":true,"$schema":"http://json-schema.org/draft-07/schema#"},"annotations":{"title":"Find members","readOnlyHint":true,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false},"execution":{"taskSupport":"forbidden"},"_meta":{"requiredScopes":["members:read","presence-statuses:read"]}},{"name":"people_mutate","description":"Create or update people resources: member (A person inside a team workspace); presenceStatus (A reusable team-scoped template for member statuses — e.g., \"In a meeting\", \"Focus time\", \"Out of office\"). Pass {resource, operation} plus the resource's writable fields. Batch writes: bulkCreate, bulkUpdate — pass {resource, operation, items:[…], onError?}. Recovering a bulkCreate depends on whether you got a result envelope back. GOT ONE: trust it — it is index-aligned, rows marked ok are created, and you may send a NEW batch containing only the rows that never got created. NO ENVELOPE (timeout / lost response): resend the SAME items in the SAME order — that exact batch is deduplicated per row and replays what already landed instead of re-creating it. Do NOT re-split it or drop items first; that changes the dedup key and DOES create duplicates. bulkUpdate carries no dedup key, so it is safe to resend only because each item is an absolute field set — never resend one whose values are relative to the current value. Concurrency-safe updates: on a single-row update, pass expectedUpdatedAt = the updatedAt from the read your edit is based on; a stale_write (409) means the row changed since — re-read it, re-derive your change, and retry with the fresh updatedAt (never a timestamp echoed by the error). The guard requires at least one field change. Bulk items do not accept it: with onError absent, one item carrying it rejects the WHOLE batch client-side (0 HTTP calls); onError:'continue' ships the valid rows and returns per-item errors. Clearing a value: a field whose type includes \"null\" accepts an explicit JSON null to clear it (for an FK, to unlink it). Omitting a key leaves that field UNCHANGED — omission and null are different requests, so send only the keys you mean to change.","inputSchema":{"type":"object","properties":{"resource":{"type":"string","enum":["member","presenceStatus"],"description":"Which resource within this domain group to act on."},"operation":{"type":"string","enum":["bulkCreate","bulkUpdate","create","update"],"description":"Which operation to run on the resource. Valid arguments depend on the (resource, operation) pair — unknown arguments are rejected with an error that names the accepted ones (the top-level schema is permissive because it forwards to a strict per-operation validator)."},"id":{"type":"string","pattern":"^[0-9A-Za-z_!]{1,16}$","description":"Entity id — for \"update\", the add{Relation}/remove{Relation} ops, and instance-target verb ops (id = the entity acted on)."},"ids":{"type":"array","items":{"type":"string","pattern":"^[0-9A-Za-z_!]{1,16}$","description":"related entity id"},"description":"Target ids for an add{Relation}/remove{Relation} op (1..500; auto-chunked)."},"items":{"type":"array","items":{"type":"object","properties":{},"additionalProperties":true},"minItems":1,"description":"bulkCreate/bulkUpdate only: array of per-item bodies, each the same shape as a single create/update (bulkUpdate items include id). Max 150."},"onError":{"type":"string","enum":["abort","continue"],"description":"bulk ops only: abort (default, stop at first failure) or continue (best-effort, per-item errors in the result)."},"expectedUpdatedAt":{"type":"string","format":"date-time","description":"update (single-row) only: concurrency guard — the `updatedAt` from the read your edit is based on. 409 stale_write if the row changed since; re-read, re-derive, retry with the fresh value. Requires ≥1 field change. Not accepted on bulk items."},"name":{"type":"string","description":"Member's display name"},"email":{"type":"string","description":"Member's email address."},"roleId":{"type":"string","description":"The member's role, which decides what they may do. Changing it re-roles the member and can cascade permission changes across the workspace; it requires the `editRole` permission. A role name (for example `manager`) is accepted in place of an id."},"theme":{"type":"string","enum":["light","dark","fullLight","full_light"],"description":"The colour theme this member sees in the Nifty app. Writable only on your own member record — writing another member's returns 403 whatever your role."},"state":{"type":["string","null"],"description":"The Nifty app's own per-member layout state (collapsed groups, hidden columns, saved filters). Not part of the public API."},"storyPointsCapacity":{"type":"number","description":"How many story points this member is expected to carry in a sprint. Writable only on your own member record."},"projectId":{"type":"string","description":"Project to add the invited member to (invite-only convenience; no backing column — the adapter seeds the ProjectMember)."},"customFields":{"type":"array","items":{"type":"object","properties":{"customFieldId":{"type":"string","minLength":1},"value":{"type":"string"}},"required":["customFieldId","value"],"additionalProperties":false},"maxItems":50,"description":"Set custom-field values on this member. Each entry is `{ customFieldId, value }` and `value` is always a string — send `\"\"` to clear one. Only on update: an invite rejects it with a 400. Read them back with `?expand=customFields`."},"emoji":{"type":["string","null"],"description":"A single emoji shown beside the status."},"appearsOnline":{"type":"boolean","description":"Whether the member is shown as online to their team while this status is active. This is an intent you set, not a live presence reading."},"muteNotifications":{"type":"boolean","description":"Whether the member's notifications are silenced while this status is active."},"defaultDurationMs":{"type":["number","null"],"description":"How long the status stays on before it expires by itself, in milliseconds — 3600000 is one hour. Omit it (or send null) for a status that never auto-expires."}},"required":["resource","operation"],"additionalProperties":true,"$schema":"http://json-schema.org/draft-07/schema#"},"annotations":{"title":"Invite or update members","readOnlyHint":false,"destructiveHint":true,"idempotentHint":false,"openWorldHint":true},"execution":{"taskSupport":"forbidden"},"_meta":{"requiredScopes":["members:write","presence-statuses:write"]}},{"name":"people_destroy","description":"Delete people resources: member (A person inside a team workspace); presenceStatus (A reusable team-scoped template for member statuses — e.g., \"In a meeting\", \"Focus time\", \"Out of office\"). Pass {resource, operation: \"delete\", id}. Batch delete: bulkDelete — pass {resource, operation: \"bulkDelete\", ids:[…], onError?}. Bulk deletes are NOT idempotent-safe on retry: deleting an already-deleted row reports resource_not_found, not success. If the response is lost, re-query the ids to see what actually remains and delete only those — do not blind-retry the batch, and do not read a resource_not_found on a lost-response retry as \"the delete failed\".","inputSchema":{"type":"object","properties":{"resource":{"type":"string","enum":["member","presenceStatus"],"description":"Which resource within this domain group to act on."},"operation":{"type":"string","enum":["bulkDelete","delete"],"description":"Which operation to run on the resource. Valid arguments depend on the (resource, operation) pair — unknown arguments are rejected with an error that names the accepted ones (the top-level schema is permissive because it forwards to a strict per-operation validator)."},"id":{"type":"string","pattern":"^[0-9A-Za-z_!]{1,16}$","description":"Entity id to delete."},"ids":{"type":"array","items":{"type":"string","pattern":"^[0-9A-Za-z_!]{1,16}$"},"minItems":1,"description":"bulkDelete only: entity ids to delete. Max 150."},"onError":{"type":"string","enum":["abort","continue"],"description":"bulk ops only: abort (default, stop at first failure) or continue (best-effort, per-item errors in the result)."}},"required":["resource","operation"],"additionalProperties":true,"$schema":"http://json-schema.org/draft-07/schema#"},"annotations":{"title":"Remove members","readOnlyHint":false,"destructiveHint":true,"idempotentHint":true,"openWorldHint":false},"execution":{"taskSupport":"forbidden"},"_meta":{"requiredScopes":["members:delete","presence-statuses:delete"]}},{"name":"reporting_query","description":"Read (list / get / search) reporting resources: projectActivity (A read-only, append-only per-project ACTIVITY FEED row: WHO triggered WHICH domain event in a project); reportChart (A team-scoped saved reporting chart that aggregates task data (live, events, or historical) across one or more projects); timeEntry (A member's time-tracked interval on a task — list/get return only YOUR entries unless your role holds viewTimeReports, which widens them to your whole team (filter `memberId` for one person; search NEVER widens, so build team reports with list)). Pass {resource, operation} plus the operation's args.","inputSchema":{"type":"object","properties":{"resource":{"type":"string","enum":["projectActivity","reportChart","timeEntry"],"description":"Which resource within this domain group to act on."},"operation":{"type":"string","enum":["get","list","search"],"description":"Which operation to run on the resource. Valid arguments depend on the (resource, operation) pair — unknown arguments are rejected with an error that names the accepted ones (the top-level schema is permissive because it forwards to a strict per-operation validator)."},"createdAt":{"type":"string","description":"When the event happened."},"createdAt_gt":{"type":"string","description":"Filter \"createdAt\" by gt (operator filter)."},"createdAt_gte":{"type":"string","description":"Filter \"createdAt\" by gte (operator filter)."},"createdAt_lt":{"type":"string","description":"Filter \"createdAt\" by lt (operator filter)."},"createdAt_lte":{"type":"string","description":"Filter \"createdAt\" by lte (operator filter)."},"projectId":{"type":"string","pattern":"^[0-9A-Za-z_!]{1,16}$","description":"Scope \"list\"/\"search\" to one project (project-scoped resources only). REQUIRED for \"search\": unlike \"list\", search has NO workspace-wide fallback — omitting it on \"search\" errors `project_not_specified`. \"list\" does NOT require it: omitting it on \"list\" falls back to a workspace-wide scan across every project you can access."},"projectId_in":{"type":"array","items":{"type":"string"},"description":"Filter \"projectId\" by in (operator filter)."},"type":{"type":"string","description":"What happened: `projectCreated`, `usersJoined`, `usersLeft`, `listCreated`, `listMilestoneCreated`, `listUnlocked`, `taskCompleted`, `fileUploaded` or `docCreated`. This value determines which of the reference ids below is set."},"type_in":{"type":"array","items":{"type":"string"},"description":"Filter \"type\" by in (operator filter)."},"taskId":{"type":"string","description":"The task this activity references. Set for `taskCompleted`; null otherwise."},"taskId_in":{"type":"array","items":{"type":"string"},"description":"Filter \"taskId\" by in (operator filter)."},"listId":{"type":"string","description":"The list this activity references. Set for `listCreated`, `listMilestoneCreated` and `listUnlocked`; null otherwise."},"listId_in":{"type":"array","items":{"type":"string"},"description":"Filter \"listId\" by in (operator filter)."},"documentId":{"type":"string","description":"The document this activity references. Set for `docCreated`; null otherwise."},"documentId_in":{"type":"array","items":{"type":"string"},"description":"Filter \"documentId\" by in (operator filter)."},"fileId":{"type":"string","description":"The file this activity references. Set for `fileUploaded`; null otherwise."},"fileId_in":{"type":"array","items":{"type":"string"},"description":"Filter \"fileId\" by in (operator filter)."},"triggeredById":{"type":"string","description":"The team member who triggered this event, or null when it was triggered by an automation, an agent or a scheduled job. A member id — resolve it with `GET /members/{id}`."},"triggeredById_in":{"type":"array","items":{"type":"string"},"description":"Filter \"triggeredById\" by in (operator filter)."},"dataType":{"type":"string","enum":["live","events","historical"],"description":"Aggregation mode for the chart: `live`, `events`, or `historical`."},"chartType":{"type":"string","enum":["line","bar","area","areaStacked","pie","table","number","funnel"],"description":"Visualization type: `line`, `bar`, `area`, `areaStacked`, `pie`, `table`, `number`, or `funnel`."},"name":{"type":"string","description":"Optional display name for the chart."},"name_contains":{"type":"string","description":"Filter \"name\" by contains (operator filter)."},"name_startsWith":{"type":"string","description":"Filter \"name\" by startsWith (operator filter)."},"start":{"type":"string","description":"When the tracked interval began (ISO 8601)."},"start_gt":{"type":"string","description":"Filter \"start\" by gt (operator filter)."},"start_gte":{"type":"string","description":"Filter \"start\" by gte (operator filter)."},"start_lt":{"type":"string","description":"Filter \"start\" by lt (operator filter)."},"start_lte":{"type":"string","description":"Filter \"start\" by lte (operator filter)."},"manual":{"type":"string","description":"True for entries created through this API; false for entries captured by the timer in the Nifty app. Server-set."},"memberId":{"type":"string","description":"The team member this time is tracked for. Set by the server from whoever is calling — you cannot log time on someone else's behalf."},"memberId_in":{"type":"array","items":{"type":"string"},"description":"Filter \"memberId\" by in (operator filter)."},"id":{"type":"string","pattern":"^[0-9A-Za-z_!]{1,16}$","description":"Entity id — required for \"get\" and for instance-target read verb ops."},"limit":{"type":"integer","exclusiveMinimum":0,"description":"Items per page for \"list\"/\"search\" (default 50). Over the server cap it is silently clamped, not rejected — a short page does NOT mean the end; page until `nextCursor` is null."},"cursor":{"type":"string","description":"Opaque pagination cursor returned by a previous \"list\"/\"search\"."},"includeTotal":{"type":"boolean","description":"Opt-in. List `total` is `null` by default for performance (an extra COUNT over the full set). Set true to get `total` = the count of the FULL filtered + ACL-scoped result set (NOT the current page; ignores limit/cursor). The `total` key is always present: integer when requested, null when not. To ask only 'how many', pass includeTotal:true with limit:1 so you don't over-fetch rows. List-only (search always returns total)."},"query":{"type":"string","description":"Full-text query — REQUIRED for operation \"search\". The \"list\" operation has NO free-text search: it filters only by the resource's filterable fields. To search by text, use operation:\"search\" with \"query\" (not a \"search\" key on \"list\")."},"expand":{"type":"string","description":"Comma-separated relations to expand inline on the returned entity/entities. Nested paths allowed with dots (e.g. members.member) up to depth 4; nesting into a relation also requires that target's read scope. Available per resource — projectActivity: project/task/list/document/file/triggeredBy; reportChart: projects/createdByActor/updatedByActor; timeEntry: member. Unknown relations rejected."},"sort":{"type":"string","description":"Sort \"list\"/\"search\" results by ONE column. Descending: prefix \"-\" (e.g. \"-updatedAt\") OR suffix \":desc\" (e.g. \"updatedAt:desc\"). Ascending: bare column (e.g. \"updatedAt\") OR suffix \":asc\". A single string — NOT separate sortBy/sortDir, and do NOT combine \"-\" with \":asc\"/\":desc\". An unlisted column is rejected with an error naming the valid ones. Sortable columns + default per resource — projectActivity: createdAt (default -createdAt); reportChart: name/createdAt (default createdAt); timeEntry: start/createdAt (default start). Those defaults are LIST-only: omit sort on \"list\" to get the default shown, but omit it on \"search\" and results come back ranked by text relevance instead. Pass sort explicitly (e.g. sort=order) to force a specific order on either."}},"required":["resource","operation"],"additionalProperties":true,"$schema":"http://json-schema.org/draft-07/schema#"},"annotations":{"title":"Find time entries, charts and project activity","readOnlyHint":true,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false},"execution":{"taskSupport":"forbidden"},"_meta":{"requiredScopes":["project-activities:read","report-charts:read","time-entries:read"]}},{"name":"reporting_mutate","description":"Create or update reporting resources: reportChart (A team-scoped saved reporting chart that aggregates task data (live, events, or historical) across one or more projects); timeEntry (A member's time-tracked interval on a task — list/get return only YOUR entries unless your role holds viewTimeReports, which widens them to your whole team (filter `memberId` for one person; search NEVER widens, so build team reports with list)). Pass {resource, operation} plus the resource's writable fields. Also links/unlinks relations via addProjects, removeProjects — pass {resource, operation, id, ids}. Batch writes: bulkCreate, bulkUpdate — pass {resource, operation, items:[…], onError?}. Recovering a bulkCreate depends on whether you got a result envelope back. GOT ONE: trust it — it is index-aligned, rows marked ok are created, and you may send a NEW batch containing only the rows that never got created. NO ENVELOPE (timeout / lost response): resend the SAME items in the SAME order — that exact batch is deduplicated per row and replays what already landed instead of re-creating it. Do NOT re-split it or drop items first; that changes the dedup key and DOES create duplicates. bulkUpdate carries no dedup key, so it is safe to resend only because each item is an absolute field set — never resend one whose values are relative to the current value. Concurrency-safe updates: on a single-row update, pass expectedUpdatedAt = the updatedAt from the read your edit is based on; a stale_write (409) means the row changed since — re-read it, re-derive your change, and retry with the fresh updatedAt (never a timestamp echoed by the error). The guard requires at least one field change. Bulk items do not accept it: with onError absent, one item carrying it rejects the WHOLE batch client-side (0 HTTP calls); onError:'continue' ships the valid rows and returns per-item errors. Clearing a value: a field whose type includes \"null\" accepts an explicit JSON null to clear it (for an FK, to unlink it). Omitting a key leaves that field UNCHANGED — omission and null are different requests, so send only the keys you mean to change.","inputSchema":{"type":"object","properties":{"resource":{"type":"string","enum":["reportChart","timeEntry"],"description":"Which resource within this domain group to act on."},"operation":{"type":"string","enum":["addProjects","bulkCreate","bulkUpdate","create","removeProjects","update"],"description":"Which operation to run on the resource. Valid arguments depend on the (resource, operation) pair — unknown arguments are rejected with an error that names the accepted ones (the top-level schema is permissive because it forwards to a strict per-operation validator)."},"id":{"type":"string","pattern":"^[0-9A-Za-z_!]{1,16}$","description":"Entity id — for \"update\", the add{Relation}/remove{Relation} ops, and instance-target verb ops (id = the entity acted on)."},"ids":{"type":"array","items":{"type":"string","pattern":"^[0-9A-Za-z_!]{1,16}$","description":"related entity id"},"description":"Target ids for an add{Relation}/remove{Relation} op (1..500; auto-chunked)."},"items":{"type":"array","items":{"type":"object","properties":{},"additionalProperties":true},"minItems":1,"description":"bulkCreate/bulkUpdate only: array of per-item bodies, each the same shape as a single create/update (bulkUpdate items include id). Max 150."},"onError":{"type":"string","enum":["abort","continue"],"description":"bulk ops only: abort (default, stop at first failure) or continue (best-effort, per-item errors in the result)."},"expectedUpdatedAt":{"type":"string","format":"date-time","description":"update (single-row) only: concurrency guard — the `updatedAt` from the read your edit is based on. 409 stale_write if the row changed since; re-read, re-derive, retry with the fresh value. Requires ≥1 field change. Not accepted on bulk items."},"dataType":{"type":"string","enum":["live","events","historical"],"description":"Aggregation mode for the chart: `live`, `events`, or `historical`."},"chartType":{"type":"string","enum":["line","bar","area","areaStacked","pie","table","number","funnel"],"description":"Visualization type: `line`, `bar`, `area`, `areaStacked`, `pie`, `table`, `number`, or `funnel`."},"dateRange":{"type":"object","additionalProperties":{},"description":"Restricts the chart to a window of time. Omitted is stored (and read back) as `{}` rather than null."},"filter":{"type":"object","additionalProperties":{},"description":"Which tasks the chart counts. Every facet is optional; omit the whole object for no filter, and note that an omitted filter is stored (and read back) as `{}` rather than null."},"series":{"type":"array","items":{},"description":"The lines, bars or slices the chart draws, in order. Omit it and the chart is created with none (stored and read back as `[]`); send it and it must have at least one entry."},"name":{"type":["string","null"],"description":"Optional display name for the chart."},"projects":{"type":"array","items":{"type":"string","minLength":1},"minItems":1,"description":"The projects whose tasks this chart aggregates. At least one is required when the chart is created. Read them back with `?expand=projects`, or edit them one at a time via `POST`/`DELETE /report-charts/{id}/projects`."},"description":{"type":["string","null"],"description":"Free-text note about what the time was spent on. This is the field `GET /time-entries/search` matches against. Supports Markdown. Embed refs: `<@memberId>` person, `<?taskId>` task, `<#projectId>` project, `<??taskGroupId>` status, `<^milestoneId>` list, `<~labelId>` label, `<$subteamId>` portfolio, `<!discussionId>` discussion. A project niceId + number (e.g. `PROJ-123`) auto-links that task; `:shortcode:` renders an emoji. Write mention/ref glyphs as plain literal text — wrapping one in backticks or other code formatting renders it as inert code instead of a resolved mention/link."},"start":{"type":"string","description":"When the tracked interval began (ISO 8601)."},"end":{"type":["string","null"],"description":"When the tracked interval ended (ISO 8601). Omit it, or send null, to create a running entry; PATCH `end` to stop it."},"projectId":{"type":["string","null"],"description":"The project this time is booked against. Set once when the entry is created: an entry cannot be moved to another project afterwards."},"taskId":{"type":"string","description":"The task this time is tracked against. Set once when the entry is created: an entry cannot be moved to another task afterwards."}},"required":["resource","operation"],"additionalProperties":true,"$schema":"http://json-schema.org/draft-07/schema#"},"annotations":{"title":"Create or update time entries and charts","readOnlyHint":false,"destructiveHint":true,"idempotentHint":false,"openWorldHint":false},"execution":{"taskSupport":"forbidden"},"_meta":{"requiredScopes":["report-charts:link","report-charts:write","time-entries:write"]}},{"name":"reporting_destroy","description":"Delete reporting resources: reportChart (A team-scoped saved reporting chart that aggregates task data (live, events, or historical) across one or more projects); timeEntry (A member's time-tracked interval on a task — list/get return only YOUR entries unless your role holds viewTimeReports, which widens them to your whole team (filter `memberId` for one person; search NEVER widens, so build team reports with list)). Pass {resource, operation: \"delete\", id}. Batch delete: bulkDelete — pass {resource, operation: \"bulkDelete\", ids:[…], onError?}. Bulk deletes are NOT idempotent-safe on retry: deleting an already-deleted row reports resource_not_found, not success. If the response is lost, re-query the ids to see what actually remains and delete only those — do not blind-retry the batch, and do not read a resource_not_found on a lost-response retry as \"the delete failed\".","inputSchema":{"type":"object","properties":{"resource":{"type":"string","enum":["reportChart","timeEntry"],"description":"Which resource within this domain group to act on."},"operation":{"type":"string","enum":["bulkDelete","delete"],"description":"Which operation to run on the resource. Valid arguments depend on the (resource, operation) pair — unknown arguments are rejected with an error that names the accepted ones (the top-level schema is permissive because it forwards to a strict per-operation validator)."},"id":{"type":"string","pattern":"^[0-9A-Za-z_!]{1,16}$","description":"Entity id to delete."},"ids":{"type":"array","items":{"type":"string","pattern":"^[0-9A-Za-z_!]{1,16}$"},"minItems":1,"description":"bulkDelete only: entity ids to delete. Max 150."},"onError":{"type":"string","enum":["abort","continue"],"description":"bulk ops only: abort (default, stop at first failure) or continue (best-effort, per-item errors in the result)."}},"required":["resource","operation"],"additionalProperties":true,"$schema":"http://json-schema.org/draft-07/schema#"},"annotations":{"title":"Delete time entries and charts","readOnlyHint":false,"destructiveHint":true,"idempotentHint":true,"openWorldHint":false},"execution":{"taskSupport":"forbidden"},"_meta":{"requiredScopes":["report-charts:delete","time-entries:delete"]}}],"prompts":[{"name":"plan_my_week","description":"Draft a realistic day-by-day plan for the week from your open, due-soon tasks.","arguments":[]},{"name":"project_status_digest","description":"Summarize a project’s status: progress, overdue items, recent shipments, and top risks.","arguments":[{"name":"projectName","description":"Name (or partial name) of the project to summarize.","required":true}]},{"name":"triage_overdue_tasks","description":"Work through your own overdue tasks and decide what to reschedule, reassign, drop, or do now.","arguments":[{"name":"projectName","description":"Optional: limit triage to a single project (by name).","required":false}]},{"name":"standup_digest","description":"Draft a Yesterday / Today / Blockers standup update from your tasks.","arguments":[]},{"name":"meeting_notes_to_tasks","description":"Turn meeting notes into well-formed tasks in a project, after your review.","arguments":[{"name":"notes","description":"The raw meeting notes to extract action items from.","required":true},{"name":"projectName","description":"Name of the project the new tasks belong in.","required":true}]},{"name":"find_and_update","description":"Find the tasks matching a description and apply a change — with a review step first.","arguments":[{"name":"what","description":"What to find, e.g. \"tasks due this week with no assignee\".","required":true},{"name":"change","description":"The change to apply, e.g. \"set the due date to Friday\".","required":true}]}],"resources":[]}