Open app

Tools

Every tool on the MCP server as it is served, with when to use it, why it is shaped this way, and what each error means.

This page opens with how the MCP server's tools are designed, then shows each one exactly as tools/list returns it and explains what a schema cannot say: when to reach for it, why it is shaped this way, and what each error means. Connecting a client and the scopes it is granted are on the MCP server page.

How the tools are designed

One tool per read or write of what the product has: tasks, lists, and the daily plan, with a search as well as a single read for tasks. That is the whole surface, and it is small on purpose: an assistant choosing among a few tools chooses correctly, and a tool that covers a whole intent needs one call where a tool per endpoint needs several.

Saves are upserts. save_task and save_list create when you omit id and update when you pass it, with any subset of fields; a field you do not send keeps its value. The assistant never picks between a create and an update for one intent, and never restates what it is not changing.

Nothing deletes. There is no delete tool. An assistant can finish a task, clear its date or move it between lists, but it cannot remove one, or a list, even when asked. Every other write can be undone by another write; a delete is the one that cannot, and an assistant acting on a misread would take your work with it. Deleting stays in the app, under your hand.

Edits are patches, and a patch is atomic. A note or a plan can be edited with patch rather than rewritten: a list of operations, each anchored on text copied from the current content, applied in order and written only if every one lands. A miss anywhere writes nothing. The anchor is the point: an edit built on a stale reading fails instead of landing, and the text you did not touch keeps its exact wording, which a whole-document rewrite from memory does not. There are two operations, replace and append, because everything else is one of them in disguise.

Errors are sentences, written for the caller. A refusal is a tool result with isError: true and one sentence: what did not hold and what to send instead. Read it again and anchor on text that is actually there. is an error message. None of them echoes your own text back. Input that fails the schema is refused before the tool runs, by the server's validator in its own words, so every limit in a schema is the real one.

Every result comes twice. As structuredContent, conforming to the tool's outputSchema, and as the same JSON in content, so a client that reads either gets the whole answer.

A task records where it came from. The product that created it, from the client you connected with, is stored once and never changes. It is provenance, not status.

Reading a tool entry

Each definition below is the JSON a client receives when it lists the server's tools, kept identical to the live server by a test. The parts of it:

  • description is the sentence an assistant reads to decide whether to call the tool. It is the interface, so it is written for the model rather than for this page.
  • annotations are the hints the MCP specification defines. readOnlyHint is true on every read. destructiveHint is true on every write because a save can overwrite a note or a whole plan; it says what a call can do to existing data, not whether a delete exists. openWorldHint is false everywhere: every tool touches only your own Taskaid data.
  • _meta.securitySchemes names the OAuth scope the tool needs. A call without it is refused with insufficient_scope, and the message names both the scope required and the scopes you were granted.
  • inputSchema and outputSchema are JSON Schema 2020-12. Every limit in them is the real one: a limit above 100 or a name past 500 characters is refused by the server's validator before the tool runs, in the validator's words rather than ours.

What every call returns. A successful call carries the result twice: as structuredContent, which conforms to outputSchema, and as text in content, the same JSON, so a client that reads either gets the whole answer. A write's text opens with one line saying what it did, then the JSON. A refusal comes back as a tool result with isError: true and one sentence of text: what did not hold and what to send instead. The sentences are quoted under each tool. None of them echoes your own text back.

References. A task is named by its reference, TASKAID-45, #45 or 45, in every tool that takes one. A list is named by its id; view_lists resolves a name to it.

Dates. when and date are calendar days, YYYY-MM-DD, read in your own time zone, so "today" is the day it is where you are. Until you have opened the web app once, that time zone is UTC.

search_tasks

Needs tasks:read.

search_tasks
{
  "name": "search_tasks",
  "title": "Search tasks",
  "description": "Search the user's tasks. A bare call returns active (uncompleted) tasks from every list. Text search is optional. Use `view_lists` to resolve a list to its id.",
  "annotations": {
    "readOnlyHint": true,
    "destructiveHint": false,
    "openWorldHint": false
  },
  "_meta": {
    "securitySchemes": [
      {
        "type": "oauth2",
        "scopes": ["tasks:read"]
      }
    ]
  },
  "inputSchema": {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "properties": {
      "completed": {
        "description": "Read finished tasks instead of active ones. Omit for active tasks.",
        "type": "boolean"
      },
      "listId": {
        "description": "Narrow to a single list. Omit to span every list.",
        "type": "string"
      },
      "limit": {
        "default": 50,
        "description": "Max results. Defaults to 50, up to 100.",
        "type": "integer",
        "minimum": 1,
        "maximum": 100
      },
      "query": {
        "description": "Narrow to tasks matching this text. Every whitespace-separated word must appear in the task's name or note (case-insensitive, any order), so extra words narrow the results rather than widening them — pass distinctive keywords, not a full sentence.",
        "type": "string"
      }
    }
  },
  "outputSchema": {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "properties": {
      "tasks": {
        "type": "array",
        "items": {
          "type": "object",
          "properties": {
            "id": {
              "type": "string"
            },
            "url": {
              "type": "string"
            },
            "name": {
              "type": "string"
            },
            "completed": {
              "type": "boolean"
            },
            "starred": {
              "type": "boolean"
            },
            "when": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Calendar date, \"YYYY-MM-DD\", or null for Inbox."
            },
            "note": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ]
            },
            "duration": {
              "anyOf": [
                {
                  "type": "number"
                },
                {
                  "type": "null"
                }
              ]
            },
            "completedAt": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ]
            },
            "updatedAt": {
              "type": "string"
            },
            "source": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ]
            },
            "list": {
              "anyOf": [
                {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "name": {
                      "type": "string"
                    },
                    "emoji": {
                      "type": "string"
                    }
                  },
                  "required": ["id", "name", "emoji"],
                  "additionalProperties": false
                },
                {
                  "type": "null"
                }
              ]
            }
          },
          "required": [
            "id",
            "url",
            "name",
            "completed",
            "starred",
            "when",
            "note",
            "duration",
            "completedAt",
            "updatedAt",
            "source",
            "list"
          ],
          "additionalProperties": false
        }
      },
      "hasMore": {
        "type": "boolean"
      }
    },
    "required": ["tasks", "hasMore"],
    "additionalProperties": false
  }
}

When to use it

Any question about what is on the plate. A bare call answers "what do I have": active tasks from every list, soonest first, Inbox last, fifty of them. hasMore is exact, so you know when the fifty were not all of it; ask for up to 100, or narrow. completed: true reads finished work instead, newest first. query matches every word you send against the name and note, case-insensitive, in any order, so extra words narrow rather than widen: send the two distinctive ones, not the user's whole sentence. Notes come shortened to 500 characters with the marker … (truncated — use view_task for the full note) inside the text, exactly where a copy would be taken from. Never write a shortened note back.

Why it is shaped this way

One search rather than one view per list or per state. While the description said only that listId was optional, models called view_lists and then one search per list, six calls for what one answers, so the description now states what a bare call covers. There is no cursor: the read is capped at limit plus one, which is what makes hasMore exact, and past the cap the way on is narrowing, by list, text or state, not paging. A call returns one half of the corpus, active or finished, because the two have different honest orderings and a merged list has none.

Errors

  • List <id> not found: the listId is not one of your lists. Resolve it again with view_lists.

view_task

Needs tasks:read.

view_task
{
  "name": "view_task",
  "title": "View a task",
  "description": "Read one task, including its whole note — search_tasks truncates notes.",
  "annotations": {
    "readOnlyHint": true,
    "destructiveHint": false,
    "openWorldHint": false
  },
  "_meta": {
    "securitySchemes": [
      {
        "type": "oauth2",
        "scopes": ["tasks:read"]
      }
    ]
  },
  "inputSchema": {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "properties": {
      "id": {
        "type": "string",
        "description": "Task reference: TASKAID-45, #45 or 45."
      }
    },
    "required": ["id"]
  },
  "outputSchema": {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "properties": {
      "id": {
        "type": "string"
      },
      "url": {
        "type": "string"
      },
      "name": {
        "type": "string"
      },
      "completed": {
        "type": "boolean"
      },
      "starred": {
        "type": "boolean"
      },
      "when": {
        "anyOf": [
          {
            "type": "string"
          },
          {
            "type": "null"
          }
        ],
        "description": "Calendar date, \"YYYY-MM-DD\", or null for Inbox."
      },
      "note": {
        "anyOf": [
          {
            "type": "string"
          },
          {
            "type": "null"
          }
        ]
      },
      "duration": {
        "anyOf": [
          {
            "type": "number"
          },
          {
            "type": "null"
          }
        ]
      },
      "completedAt": {
        "anyOf": [
          {
            "type": "string"
          },
          {
            "type": "null"
          }
        ]
      },
      "updatedAt": {
        "type": "string"
      },
      "source": {
        "anyOf": [
          {
            "type": "string"
          },
          {
            "type": "null"
          }
        ]
      },
      "list": {
        "anyOf": [
          {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "emoji": {
                "type": "string"
              }
            },
            "required": ["id", "name", "emoji"],
            "additionalProperties": false
          },
          {
            "type": "null"
          }
        ]
      }
    },
    "required": [
      "id",
      "url",
      "name",
      "completed",
      "starred",
      "when",
      "note",
      "duration",
      "completedAt",
      "updatedAt",
      "source",
      "list"
    ],
    "additionalProperties": false
  }
}

When to use it

To read one task whole, above all its note. search_tasks shortens notes; this does not. Reach for it before editing a note with patch, since an edit anchors on the note's current text.

Why it is shaped this way

The note is where a task remembers its context, and it can be long. Paying for every note on every search would make a browse expensive, so the full read is its own call, and the search says in-band when it has held something back.

Errors

  • Task <ref> not found: no task of yours has that reference. A reference from another account is refused the same way.

save_task

Needs tasks:write.

save_task
{
  "name": "save_task",
  "title": "Create or update a task",
  "description": "Upsert a task. Omit `id` to create a new one (name required). Pass `id` to update an existing task — any subset of fields. To change part of the note, pass `patch` instead of `note`. Use null on `listId` to unlink a task from its list. Returns the saved task.",
  "annotations": {
    "readOnlyHint": false,
    "destructiveHint": true,
    "openWorldHint": false
  },
  "_meta": {
    "securitySchemes": [
      {
        "type": "oauth2",
        "scopes": ["tasks:write"]
      }
    ]
  },
  "inputSchema": {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "properties": {
      "id": {
        "description": "Task reference: TASKAID-45, #45 or 45. Pass to update an existing task; omit to create a new one.",
        "type": "string"
      },
      "name": {
        "description": "Required when creating. On update, omit to keep the current name.",
        "type": "string",
        "minLength": 1,
        "maxLength": 500
      },
      "note": {
        "description": "Task notes, as Markdown, up to 15,000 characters — rendered as rich text in the app. Headings, lists, bold, and inline code are supported. Send literal newlines and characters, not escape sequences.",
        "anyOf": [
          {
            "type": "string"
          },
          {
            "type": "null"
          }
        ]
      },
      "patch": {
        "description": "Edits to the note, applied in order and atomically — if one fails, nothing is written. Only on update, in place of `note`: text you do not send keeps its exact wording.",
        "minItems": 1,
        "maxItems": 50,
        "type": "array",
        "items": {
          "oneOf": [
            {
              "type": "object",
              "properties": {
                "op": {
                  "type": "string",
                  "const": "replace"
                },
                "old_string": {
                  "type": "string",
                  "minLength": 1,
                  "description": "Exact text to replace, copied from the current content. Must appear exactly once unless replace_all is set."
                },
                "new_string": {
                  "type": "string",
                  "description": "Replacement text. An empty string deletes the match."
                },
                "replace_all": {
                  "description": "Replace every occurrence instead of requiring the match to be unique.",
                  "type": "boolean"
                }
              },
              "required": ["op", "old_string", "new_string"]
            },
            {
              "type": "object",
              "properties": {
                "op": {
                  "type": "string",
                  "const": "append"
                },
                "text": {
                  "type": "string",
                  "minLength": 1,
                  "description": "Text to add at the end of the content. Include its own leading separator, e.g. a blank line."
                }
              },
              "required": ["op", "text"]
            }
          ]
        }
      },
      "when": {
        "description": "The day the task is scheduled for, as \"YYYY-MM-DD\" (e.g. \"2026-05-21\"). Omit or pass null when creating to leave the task in Inbox; pass null when updating to move it back there.",
        "anyOf": [
          {
            "type": "string"
          },
          {
            "type": "null"
          }
        ]
      },
      "listId": {
        "description": "Pass null to unlink the task from its list.",
        "anyOf": [
          {
            "type": "string"
          },
          {
            "type": "null"
          }
        ]
      },
      "completed": {
        "description": "Marks the task done. True stamps `completedAt` with the time of the call, never the time the work finished.",
        "type": "boolean"
      },
      "starred": {
        "type": "boolean"
      },
      "duration": {
        "description": "Estimated duration in minutes, up to a day. Null clears the estimate.",
        "anyOf": [
          {
            "type": "integer",
            "minimum": 1,
            "maximum": 1440
          },
          {
            "type": "null"
          }
        ]
      }
    }
  },
  "outputSchema": {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "properties": {
      "id": {
        "type": "string"
      },
      "url": {
        "type": "string"
      },
      "name": {
        "type": "string"
      },
      "completed": {
        "type": "boolean"
      },
      "starred": {
        "type": "boolean"
      },
      "when": {
        "anyOf": [
          {
            "type": "string"
          },
          {
            "type": "null"
          }
        ],
        "description": "Calendar date, \"YYYY-MM-DD\", or null for Inbox."
      },
      "note": {
        "anyOf": [
          {
            "type": "string"
          },
          {
            "type": "null"
          }
        ]
      },
      "duration": {
        "anyOf": [
          {
            "type": "number"
          },
          {
            "type": "null"
          }
        ]
      },
      "completedAt": {
        "anyOf": [
          {
            "type": "string"
          },
          {
            "type": "null"
          }
        ]
      },
      "updatedAt": {
        "type": "string"
      },
      "source": {
        "anyOf": [
          {
            "type": "string"
          },
          {
            "type": "null"
          }
        ]
      },
      "list": {
        "anyOf": [
          {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "emoji": {
                "type": "string"
              }
            },
            "required": ["id", "name", "emoji"],
            "additionalProperties": false
          },
          {
            "type": "null"
          }
        ]
      }
    },
    "required": [
      "id",
      "url",
      "name",
      "completed",
      "starred",
      "when",
      "note",
      "duration",
      "completedAt",
      "updatedAt",
      "source",
      "list"
    ],
    "additionalProperties": false
  }
}

When to use it

Creating and updating are one tool. Omit id and it creates: name is required, and when omitted or null leaves the task in Inbox. Pass id and it updates that task with whatever subset of fields you send; a field you do not send keeps its value. when: null moves a task back to Inbox, listId: null unlinks it from its list, duration: null clears the estimate, and completed: true finishes it, stamping completedAt with the time of the call. Send note to replace the note whole, or patch to change part of it, never both. The task records which product created it, from the client you connected with, and that never changes.

Why it is shaped this way

An upsert, so the assistant never chooses between two tools for one intent, and an update never has to restate what it is not changing. patch exists because an assistant that can only write a whole note rewrites the whole note, and the parts it did not mean to touch come back subtly reworded. A patch is a list of operations, each anchored on text copied from the current note. They are applied in order to the note in memory, and the note is written only if every one lands, conditionally on the exact bytes they were applied to. A miss anywhere means nothing was written. The note cap of 15,000 characters is per write, not per note: a patch can grow a note past it. It is enforced on arrival rather than in the schema so the refusal is one we write.

Errors

  • name is required when creating a task (omit id to create): you omitted id and name. Send a name, or the id of the task you meant to update.
  • Task <ref> not found: the id is not one of your tasks.
  • List <id> not found: the listId is not one of your lists. Resolve it with view_lists.
  • Title required: name was empty or whitespace.
  • Notes hold up to 15,000 characters; this one is <n>.: shorten the note, or send the change as a patch.
  • Send `note` to replace the note, or `patch` to edit it — exactly one of the two.: you sent both.
  • `patch` edits an existing note, so it needs `id`.: you sent a patch without saying which task.
  • This task has no note to patch. Send `note` to write one.: there is nothing to anchor on yet.
  • The note changed since you read it — nothing was written.: another writer got there first. The note you anchored on is stale; view_task has the current one.
  • Patch operation <n> did not match: its old_string is not present in the current content. Read it again and anchor on text that is actually there.: the note is not what you remember. Read it with view_task and anchor again.
  • Patch operation <n> matched <k> times. Extend old_string until it is unique, or set replace_all to change every occurrence.: the anchor was ambiguous.

view_lists

Needs tasks:read.

view_lists
{
  "name": "view_lists",
  "title": "View lists",
  "description": "Read the user's lists.",
  "annotations": {
    "readOnlyHint": true,
    "destructiveHint": false,
    "openWorldHint": false
  },
  "_meta": {
    "securitySchemes": [
      {
        "type": "oauth2",
        "scopes": ["tasks:read"]
      }
    ]
  },
  "inputSchema": {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "properties": {}
  },
  "outputSchema": {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "properties": {
      "lists": {
        "type": "array",
        "items": {
          "type": "object",
          "properties": {
            "id": {
              "type": "string"
            },
            "emoji": {
              "type": "string"
            },
            "name": {
              "type": "string"
            }
          },
          "required": ["id", "emoji", "name"],
          "additionalProperties": false
        }
      }
    },
    "required": ["lists"],
    "additionalProperties": false
  }
}

When to use it

To turn a list's name into its id before a search_tasks narrowed to that list or a save_task that files into it. A list is an emoji and a name; this returns all of yours.

Why it is shaped this way

Lists have no reference of their own because an assistant rarely names one without having just read them. There are few, and reading them is cheap.

Errors

None. A user with no lists gets an empty array.

save_list

Needs tasks:write.

save_list
{
  "name": "save_list",
  "title": "Create or update a list",
  "description": "Upsert a list. Omit `id` to create a new one (name required). Pass `id` to rename or change the emoji on an existing list. Returns the saved list.",
  "annotations": {
    "readOnlyHint": false,
    "destructiveHint": true,
    "openWorldHint": false
  },
  "_meta": {
    "securitySchemes": [
      {
        "type": "oauth2",
        "scopes": ["tasks:write"]
      }
    ]
  },
  "inputSchema": {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "properties": {
      "id": {
        "description": "List id. Pass to update an existing list; omit to create a new one.",
        "type": "string"
      },
      "name": {
        "description": "Required when creating. On update, omit to keep the current name.",
        "type": "string",
        "minLength": 1,
        "maxLength": 120
      },
      "emoji": {
        "description": "A single emoji shown before the list name. Defaults to 📋 when creating; omit on update to keep the current one.",
        "type": "string",
        "minLength": 1,
        "maxLength": 8
      }
    }
  },
  "outputSchema": {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "properties": {
      "id": {
        "type": "string"
      },
      "emoji": {
        "type": "string"
      },
      "name": {
        "type": "string"
      }
    },
    "required": ["id", "emoji", "name"],
    "additionalProperties": false
  }
}

When to use it

Omit id to create a list, with a name and optionally an emoji, which defaults to 📋. Pass id to rename one or change its emoji.

Why it is shaped this way

The same upsert as save_task, on the one other object you organise by. There is nothing else to set on a list: no sub-lists, statuses or permissions.

Errors

  • name is required when creating a list (omit id to create): you omitted id and name.
  • List <id> not found: the id is not one of your lists.
  • Name required, Emoji required: the field you sent was empty.

view_daily_plan

Needs plans:read.

view_daily_plan
{
  "name": "view_daily_plan",
  "title": "View daily plan",
  "description": "Read the user's plan for a given day. Defaults to today. Useful before regenerating so an agent can see what is already in place.",
  "annotations": {
    "readOnlyHint": true,
    "destructiveHint": false,
    "openWorldHint": false
  },
  "_meta": {
    "securitySchemes": [
      {
        "type": "oauth2",
        "scopes": ["plans:read"]
      }
    ]
  },
  "inputSchema": {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "properties": {
      "date": {
        "description": "Calendar day to read the plan for, as \"YYYY-MM-DD\" (e.g. \"2026-05-28\"). A full ISO datetime is also accepted — only the date component is used. Defaults to today.",
        "type": "string"
      }
    }
  },
  "outputSchema": {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "properties": {
      "plan": {
        "anyOf": [
          {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "url": {
                "type": "string"
              },
              "date": {
                "type": "string",
                "format": "date",
                "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$",
                "description": "Calendar date, \"YYYY-MM-DD\"."
              },
              "content": {
                "type": "string"
              }
            },
            "required": ["id", "url", "date", "content"],
            "additionalProperties": false
          },
          {
            "type": "null"
          }
        ]
      }
    },
    "required": ["plan"],
    "additionalProperties": false
  }
}

When to use it

To read the plan for a day before writing or editing it. date defaults to today. When no plan exists for that day, plan is null and the text says so.

Why it is shaped this way

The result is wrapped, { plan }, so that a day with no plan can be said plainly as null; a bare object cannot express absence. Read it before a patch, since a patch anchors on the plan's current text, and before rewriting one, so the assistant can see what is already in place.

Errors

None. A day with no plan is plan: null, not an error.

set_daily_plan

Needs plans:write.

set_daily_plan
{
  "name": "set_daily_plan",
  "title": "Set daily plan",
  "description": "Write or edit the user's plan for a day. Send `content` for the whole plan or `patch` to change part of it — exactly one of the two. Read the user's tasks first for context.",
  "annotations": {
    "readOnlyHint": false,
    "destructiveHint": true,
    "openWorldHint": false
  },
  "_meta": {
    "securitySchemes": [
      {
        "type": "oauth2",
        "scopes": ["plans:write"]
      }
    ]
  },
  "inputSchema": {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "properties": {
      "date": {
        "description": "Calendar day to write the plan for, as \"YYYY-MM-DD\" (e.g. \"2026-05-28\"). A full ISO datetime is also accepted — only the date component is used. Defaults to today.",
        "type": "string"
      },
      "content": {
        "description": "The whole day's plan, as Markdown — headings, lists, bold, inline code. Send literal newlines and characters, not escape sequences. Replaces any existing plan; to change part of one, send `patch`.",
        "type": "string",
        "minLength": 1,
        "maxLength": 5000
      },
      "patch": {
        "description": "Edits to the plan, applied in order and atomically — if one fails, nothing is written. Prefer this to resending `content`: text you do not send keeps its exact wording.",
        "minItems": 1,
        "maxItems": 50,
        "type": "array",
        "items": {
          "oneOf": [
            {
              "type": "object",
              "properties": {
                "op": {
                  "type": "string",
                  "const": "replace"
                },
                "old_string": {
                  "type": "string",
                  "minLength": 1,
                  "description": "Exact text to replace, copied from the current content. Must appear exactly once unless replace_all is set."
                },
                "new_string": {
                  "type": "string",
                  "description": "Replacement text. An empty string deletes the match."
                },
                "replace_all": {
                  "description": "Replace every occurrence instead of requiring the match to be unique.",
                  "type": "boolean"
                }
              },
              "required": ["op", "old_string", "new_string"]
            },
            {
              "type": "object",
              "properties": {
                "op": {
                  "type": "string",
                  "const": "append"
                },
                "text": {
                  "type": "string",
                  "minLength": 1,
                  "description": "Text to add at the end of the content. Include its own leading separator, e.g. a blank line."
                }
              },
              "required": ["op", "text"]
            }
          ]
        }
      }
    }
  },
  "outputSchema": {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "properties": {
      "id": {
        "type": "string"
      },
      "url": {
        "type": "string"
      },
      "date": {
        "type": "string",
        "format": "date",
        "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$",
        "description": "Calendar date, \"YYYY-MM-DD\"."
      },
      "content": {
        "type": "string"
      }
    },
    "required": ["id", "url", "date", "content"],
    "additionalProperties": false
  }
}

When to use it

To write the day's plan, or to change part of it. Send content to write the whole plan, up to 5,000 characters of Markdown, replacing whatever was there. Send patch to change part of an existing plan, keeping every word you did not touch. Exactly one of the two. There is at most one plan per day. Read the user's tasks first: a plan is written from what is actually on the plate.

Why it is shaped this way

The plan is a short piece of writing, and the way it is edited through the day, a task finished, a meeting moved, is a small change to a text the user may have written themselves. patch keeps their wording. It works exactly as on a task note: operations applied in order to the plan in memory, written only if all of them land, conditionally on the text they were applied to. If the plan changed under the patch, nothing is written and the message says to read it again. The one-of-two rule is enforced on arrival rather than in the schema because JSON Schema cannot express it, and this way the message is ours.

Errors

  • Send `content` to write the whole plan, or `patch` to edit it — exactly one of the two.: you sent both, or neither.
  • No plan exists for that day yet, so there is nothing to patch. Send `content` to write one.: a patch needs a plan to anchor on.
  • The plan changed while this patch was being applied. Read it again and retry.: another writer got there first. Read the plan with view_daily_plan and anchor again.
  • Patch operation <n> did not match: its old_string is not present in the current content. Read it again and anchor on text that is actually there.: the plan is not what you remember.
  • Patch operation <n> matched <k> times. Extend old_string until it is unique, or set replace_all to change every occurrence.: the anchor was ambiguous.