TaskLite · Developer Manual
Building on TaskLite
TaskLite gives you two ways in, and almost every wasted afternoon starts by reaching for the wrong one. MCP is how you build the backend. The app API is how your application talks to it. This manual covers both, all 48 tools, and how to read a failure that arrives disguised as a success.
Prefer a walkthrough? The step-by-step guides build a backend, a CRM and an inventory app from the first prompt to a working admin.
@tasklite/mcp 0.14.3 · current release, generally availableapi.tasklite.netRevised 15 September 2026
Start here
Two channels, and which one you want
Both channels reach the same boards and the same rows. They differ in who holds the credential and who is making the call.
MCP
For you and Claude
An agent protocol. Claude Code holds your personal key (tl_…) and works directly on projects, boards, columns and rows. This is how the system gets designed and built, and how you seed and repair data afterwards.
get_board_schema · create_item · set_cell
App API
For your application
Plain REST over HTTPS. Your server holds an app key (tk_…) and calls published endpoints. A Next.js route or any other backend uses this, a web app cannot speak MCP, and should not try.
GET · POST · PATCH · DELETE
The confusion is understandable: MCP contains the tools that create the app API, create_app, create_app_endpoint, create_app_api_key. Those are MCP tools for setting up the second channel, not a requirement of the first. You do not need an app to use MCP.
Setup
Connecting
Three ways in, easiest first. They reach the same tools, pick by where you want to work.
The connector, nothing to install
In Claude (claude.ai or the desktop app), open Settings → Connectors, find TaskLite in the directory, and add it. You sign in once and Claude keeps the connection, there is no key to create, copy or store. If you would rather add it by address, use https://mcp.tasklite.net/mcp.
After connecting on claude.ai, the tools appear in a new chat, an existing conversation will not pick them up. That trips up more people than any other step.
One-click install
The hosted server, added to your client with its OAuth sign-in. Nothing to paste.
Claude Code, in your terminal
Install by binary, npx -y is mis-parsed on Windows.
npm install -g @anthropic-ai/claude-code @tasklite/mcp
claude mcp add tasklite -- tasklite-mcpAdd -s user to make it available in every project rather than only the current one. With an existing account, pass a key created at TaskLite → Integrations → Connect Claude Code:
claude mcp add tasklite -e TASKLITE_API_KEY=tl_xxx -- tasklite-mcpChatGPT, Gemini, Cursor, Codex, VS Code, Windsurf, the OpenAI and Gemini APIs
All of them speak MCP, and the hosted server already has what each one needs: OAuth with dynamic client registration for the clients that sign you in through a browser (ChatGPT, Gemini CLI, VS Code, Cursor), bearer API keys for the ones that do not (the OpenAI Responses API and Agents SDK, the Gemini API SDK, CI), and the search and fetch tools ChatGPT requires. Config snippets for every client are in the client setup guide.
Any other MCP client
Point it at the hosted server with a personal key:
claude mcp add --transport http tasklite https://mcp.tasklite.net/mcp \
--header "Authorization: Bearer tl_xxx"The hosted server is stateless, every request carries its own credential, so one endpoint serves every account.
If any tool returns an auth error, call connection_status first, it tells you whether this machine is connected at all.
The sequence matters
Build order
Each step needs identifiers produced by the one before it. Skipping ahead is the most common cause of rows that save but hold nothing.
- 1create_project, one project models one business process.
- 2create_board, a board is a table inside that project.
- 3create_column per field. Choose types by meaning: a status field is a
dropdownwith real options, never free text. Types carry the whole UX,dateunlocks calendar and timeline views,phonegives tap-to-call,currencyformats money. - 4get_board_schema, read back the real column ids. Cells are keyed by id, and a guessed name writes nothing.
- 5create_item with the title and every cell in one call. Reach for set_cell only to change a value later, and for update_column when the model itself turns out wrong.
Boards double as the admin interface your client operates, so this is a real system from the first column, not a staging area for one.
The part people get wrong
Wiring an external frontend
The host
The API is served from api.tasklite.net. Nothing else. app.tasklite.net is the admin interface, a different origin entirely. Take the baseUrl that get_app_spec returns and use it verbatim, do not rebuild it from the URL you see in the browser.
Four things must be true
| Requirement | Set by | If missing |
|---|---|---|
| The app is published | publish_app | 401 App not found or not published |
| The endpoint exists and is active | create_app_endpoint | 401 Endpoint not found |
| A valid, unexpired API key | create_app_api_key | 401 API key required |
| The endpoint exposes columns | exposedColumns | Reads return metadata only; writes are refused |
Calling it
Two ways, and which one you need depends on where the call comes from.
From a phone or a browser, the caller is the person using it, and they send the token they got from signing in. Nothing secret sits on the device, and there is no server of yours in the middle:
curl -i -H "Authorization: Bearer <the jwt from /auth/login>" \
https://api.tasklite.net/apps/app-0cu5fs/api/board5The token decides who the caller is, so nothing in the request can name someone else, and row-level security applies to it. See Calling straight from a phone for what that means.
From a server of yours, a Next.js route, a cron job, another backend, use the app key instead:
curl -i -H "x-api-key: tk_xxx" \
https://api.tasklite.net/apps/app-0cu5fs/api/board5Authorization: Bearer tk_… works for the key too. A key is the app speaking, not a person, so it must stay server-side: an environment variable read by an API route, never anything the browser can see. If you have no server, you do not need one, use the first form.
Querying
A list endpoint takes page and pageSize (up to 100) and returns { items, total, page, pageSize }. Filter on any item field or exposed column by alias, name or id:
GET /apps/{app}/api/orders?filter[status]=open&filter[total][gte]=500&sort=-createdAt
GET /apps/{app}/api/orders?filter[customer][in]=<id1>,<id2>&filter[invoice][empty]=true
GET /apps/{app}/api/orders?search=cohen&pageSize=50
GET /apps/{app}/api/orders/<itemId> # one rowOperators: eq (the default), ne, gt, gte, lt, lte, contains, in (comma list), empty (true or false). Conditions AND together. Numeric and currency columns compare as numbers, relation columns by item id, everything else as text. An unknown key or operator answers 400 listing what is allowed.
Live updates
Push reaches someone whose app is closed. This is the other half: the message that should appear while they are looking at the screen, without asking the server every few seconds whether anything happened.
GET /apps/<app>/api/<endpoint>/stream
Accept: text/event-stream
Authorization: Bearer <the jwt from /auth/login>
event: ready
data: {"endpoint":"messages","syncedAt":"2026-09-08T09:14:02.881Z", ...}
event: created
id: 2026-09-08T09:15:44.102Z
data: {"id":"9c2...","boardId":"...","title":"See you at six","updatedAt":"..."}Three event types arrive: created, updated and deleted. A comment line every 25 seconds keeps the connection from being cut as idle.
It carries what changed, not the whole row. A client that needs the new values calls the endpoint with ?since= and the timestamp it was last given, which is the same catch-up path it uses after being offline. One mechanism covers both, and an event missed while the connection was down is not a lost message.
Advance your cursor as events arrive: each one carries an id, and that id is the timestamp to send as since next time. A client that keeps only the syncedAt from when it first connected will still lose nothing, but every reconnect will re-fetch the whole session instead of the gap.
Row-level security applies. A stream on a per-user endpoint only ever mentions that user’s rows, and it needs to know who is listening: the end user’s own token, or the app key with X-App-User. Without one it answers 401 rather than opening a feed it would have to fill with everyone’s data.
Writes made anywhere reach the stream, including edits people make in the TaskLite admin. A coach replying from their desk appears on the athlete’s phone; that is usually the point.
An app may hold 500 open streams at once. Past that the stream closes with an error event telling the client to fall back to ?since= polling, which stays correct at any scale, just less immediate.
Push notifications
A live connection reaches a phone only while its screen is on and the app is in front of the user; the operating system closes it as soon as the app goes to the background. Everything worth telling someone: a message arrived, a session is in an hour, a task was assigned, happens while the app is closed. That is what push is for, and it is why it exists here before a live connection does.
The credentials are yours. Notifications go out through your Firebase project, so they carry your app's name and icon and you keep control of the project. Download the service account from Firebase console → Project settings → Service accounts → Generate new private key, and post the file unchanged:
POST /organizations/<org>/apps/<appId>/push/credentials
{ "serviceAccount": { …the JSON file, exactly as downloaded… } }
GET /organizations/<org>/apps/<appId>/push # what is configured, how many devices
POST /organizations/<org>/apps/<appId>/push/test # { "userIds": ["…"] }, prove it worksThe private key is encrypted before it is stored and is never returned by any endpoint. If the server has no encryption key configured, the request is refused rather than storing the key in the clear.
The phone registers itself. On every launch, after sign-in, send the token Firebase gave the app. Re-sending the same token moves it to the calling user, so a phone that changes hands stops delivering the previous owner's notifications. Delete it on sign-out.
POST /apps/<app>/api/devices { "token": "<fcm token>", "platform": "android" }
DELETE /apps/<app>/api/devices/<token>
GET /apps/<app>/api/devices # this user's devices, without the tokensThese need to know who is calling, the end user's own token (Authorization: Bearer <jwt>), or the app key together with X-App-User. A device belongs to a person, so an anonymous call has nobody to attach it to and answers 401.
Sending. Add a send_push action to an automation. By default it goes to the row's owner, which is usually the whole answer: a workout assigned to someone is a row that belongs to them. recipientsFromRelation follows a relation or people column instead, a message row pointing at its coach, say, and userIds names people outright. Title and body take the same column placeholders as the other actions.
A token the service reports as dead is deleted rather than retried forever, and a delivery failure never fails the write that triggered it: the automation log records what happened.
Offline and retries
A phone in a gym basement is the normal case, not the edge case. Three things make a client safe to write from there.
Catch up. ?since=<timestamp> turns a list into a change feed: the rows that moved after that moment, oldest first, plus deleted with the ids of rows removed since then, and syncedAt. Pass syncedAt back as the next since and nothing falls between two calls. It combines with the filters and with paging.
Retry safely. Send Idempotency-Key on a POST and the same key returns the first answer rather than writing a second row, so a request that timed out can simply be sent again. Keys are remembered for seven days. The same key with a different body answers 409 instead of quietly returning the wrong row.
Send the queue. POST …/<endpoint>/batch takes up to 50 operations - { op: "create" | "update" | "delete", id?, data?, clientId? }, and reports each one separately, so one rejected row does not discard the other forty-nine. An operation with a clientId is idempotent under that id, which means replaying the whole queue writes nothing twice. Each operation still obeys the methods the endpoint allows.
# what changed while I was away
GET /apps/gym/api/sets?since=2026-09-07T06:00:00.000Z
→ { items: [...], deleted: ["b1f0…"], syncedAt: "2026-09-07T09:14:02.881Z" }
# the queue that piled up, sent once
POST /apps/gym/api/sets/batch
{ "operations": [
{ "op": "create", "clientId": "local-41", "data": { "reps": 8 } },
{ "op": "update", "clientId": "local-42", "id": "9c2…", "data": { "reps": 10 } }
] }
→ { "succeeded": 2, "failed": 0, "results": [ … ] }Nested rows in one row
A workout has exercises and an exercise has sets; an order has lines. A repeater column holds that directly: an array of objects with a declared shape, stored on the row, read and written in one call. Two boards linked by a relation is the other way to model it and is right when each exercise is a thing in its own right that people open, filter and report on. A repeater is right when the rows only exist inside their parent.
The shape is declared in the column settings as subFields, a list of { key, label, type }. Set isGroup: true for a single object instead of a list. The OpenAPI document for your app describes the exact shape, so a generated client gets a real type rather than a string:
"exercises": [
{ "name": "Bench press", "reps": 8, "weight": 60, "done": true },
{ "name": "Squat", "reps": 5, "weight": 90, "done": false }
]Sub-field types follow the same meanings as column types: number, rating, duration and currency arrive as numbers, checkbox as a boolean, everything else as a string. A repeater with no declared sub-fields is still accepted, and the spec then says only that the entries are objects, because nothing has promised what is in them.
Files
A file column exposed on an endpoint gets four calls under the row: POST …/<id>/files/<field>/upload-url with { fileName, mimeType, fileSize } returns a presigned PUT and a fileKey; upload the bytes to that URL; then POST …/files/<field>/attach with the fileKey records it on the row. GET …/files/<field> lists files with short-lived download URLs, and DELETE …/files/<field>/<attachmentId> removes one. Bytes never pass through the API.
Thumbnails. That last sentence is also the answer to resizing: the bytes go straight from the device to storage and back, so there is no point at which we could shrink an image even if we wanted to. Make the thumbnail on the client, where the picture already is, and attach both to the same column: progress.jpg and progress.thumb.jpg. A file column holds a list, so the gallery reads the small one and the detail view reads the large one, and a phone on a slow connection never downloads a 4 MB photo to draw it 80 pixels wide.
Limits. 100 MB per file. The type is checked twice: against the mimeType you declare, and against the bytes that actually arrived, so a renamed executable is rejected at attach time. Allowed types:
- Images,
image/jpeg,image/png,image/gif,image/webp,image/svg+xml - Documents,
application/pdf, Word, Excel and PowerPoint (both the old and the Open XML types),text/plain,text/csv - Video,
video/mp4,video/webm,video/quicktime,video/x-msvideo,video/x-matroska,video/3gpp,video/mpeg - Archives,
application/zip, RAR, 7z
Anything else answers 400 listing the allowed types. Storage is metered against the plan quota, not the per-file limit. Files are stored as uploaded: there is no server-side resizing or thumbnail generation yet, so generate the sizes you need on the client and attach them as separate files.
Required columns
Columns marked required on the board are enforced here: a POST without one, or a PATCH that blanks one, answers 400 naming the field. Writes are atomic, the row and every cell commit together or not at all.
Validation rules
Every column checks its own type on write, a number column refuses text, an email column refuses nope, a dropdown refuses a value that is not one of its options. On top of that, settings.validation on a column adds rules that hold on every write path, the UI and the MCP included:
{ "validation": { "unique": true, "min": 0, "max": 100000,
"minLength": 3, "maxLength": 32,
"pattern": "^[A-Z]{2}-\d{4}$", "patternMessage": "Format is XX-0000" } }A violation answers 400 with the column name and the rule: "SKU": "AB1" already exists on this board (column is unique).
Changing a column’s type
update_column with a new type converts the stored values: number and currency both ways, text into number, date or checkbox, anything into text. Values that cannot convert are cleared, never left in the old shape, and the response says { converted, cleared }. Relation, people and file columns cannot change type.
Every app publishes its own OpenAPI
GET /apps/<app>/api/openapi.json with the app key returns an OpenAPI 3.1 document for that app: every endpoint with list, single row, create, update, delete and file routes, field schemas derived from the column types (enums for closed choices, the validation rules as constraints), the query grammar, auth and the error shape. Open it in Scalar or Postman, or feed it to a client generator. The same document is get_app_spec’s sibling at …/apps/<appId>/spec/openapi for signed-in users.
exposedColumns is not optional in practice
It decides which columns are readable and writable. An endpoint without it answers GET with only id, title, status, priority and timestamps, none of your actual fields. Give every endpoint the column ids from get_board_schema, and set an alias when you want a stable key name in the JSON.
Per-user data
When your app has its own users, set rowLevelSecurity.enabled on the endpoint. Your server then sends X-App-User: <your user id> alongside the API key, and the endpoint returns, updates and deletes only that user’s rows, refusing the request outright when the header is absent.
Your app’s users
Two ways, and they combine. Bring your own sign-in: keep Auth0, Clerk, NextAuth or your own sessions, and send the signed-in user’s stable id as X-App-User from your server. TaskLite mints a matching external user on first sight and row-level security scopes every read and write to it. Or let TaskLite be the user store: your app calls the sign-up and sign-in endpoints, keeps the returned token, and passes the user id the same way.
# 1. sign-up (open or admin-approved, configure_external_access decides)
POST https://api.tasklite.net/auth/register-external
{ "organizationId": "<orgId>", "name": "Dana Levi", "email": "[email protected]", "password": "…" }
→ { "user": { "id": "u_…" }, "token": "<jwt>" } # or pendingApproval: true
# 2. sign-in
POST https://api.tasklite.net/auth/login { "email": "[email protected]", "password": "…" }
→ { "user": { "id": "u_…" }, "token": "<jwt>" }
# 3. from YOUR server, on every App API call for that user
GET https://api.tasklite.net/apps/<app>/api/orders
Authorization: Bearer tk_… # the app key, server-side only
X-App-User: u_… # the user id from step 1 or 2The app key never reaches the browser, and a user only ever sees rows written under their own id. Roles for app users (viewer, editor, submitter) are set per app.
Signing in with Google works for these users too, which on Android is the way people expect to get in. Get an ID token from Google on the client, then hand it over:
POST https://api.tasklite.net/auth/google-external
{ "credential": "<the Google ID token>", "organizationId": "<orgId>" }
→ { "user": { "id": "u_…" }, "token": "<jwt>" } # or pendingApproval: trueRegister your own Google client id first. Put it on the organization as settings.googleClientId. Without it we fall back to TaskLite's own client, which puts our name on the consent screen and, on Android, does not work at all: Google ties the token to the requesting app's package name and signing certificate, and your app is not in our Google project. Both are accepted, so anything built before this keeps working.
The token is verified against that client id, and the same registration policy applies as for email sign-up: open lets them in at once, approval holds them until an admin approves, closed refuses self-signup. Somebody who already has an account with that email simply signs in. A credential Google does not recognise answers 401, and an unknown organization 404.
The token that comes back is the one the app then sends as Authorization: Bearer on every App API call, so Google sign-in and calling straight from a phone are two halves of the same flow.
Calling straight from a phone
Everything above assumes a server of yours in the middle, holding the key. A mobile app has no such place. A phone cannot keep a secret, and a shared key plus X-App-User means anyone holding the key can read anyone’s rows by editing a header.
So an end user may call the App API with the token they got from signing in, and then they are simply themselves:
GET https://api.tasklite.net/apps/<app>/api/workouts
Authorization: Bearer <the jwt from /auth/login> # no app key on the deviceThe token decides who the caller is, so nothing in the request can name a different user — an X-App-User header sent alongside it is ignored. The organization is still the boundary: a valid TaskLite session belonging to someone else is not a user of your app and answers 401. Blocked and deleted accounts are refused, and the per-app role still decides what the person may do.
A signed-in caller also gets their own rate-limit allowance rather than sharing the app’s, so one person on their phone cannot spend everyone else’s and cannot be starved by them. Authorization: Bearer tk_… still means the app key, and still belongs on a server.
Audit trail
Every create, update, delete, status change, assignment, login and export is recorded with who, when, from which address. Owners and admins read their own organization’s trail:
GET /organizations/<orgId>/audit-logs?entityType=item&entityId=<itemId>&startDate=2026-09-01&limit=50Export
The whole project as one JSON document: boards, columns with their settings, items with cells keyed by column id, from GET /organizations/<orgId>/projects/<projectId>/export.json or the export_project tool. It is the shape a migration or a backup needs, and the one an agent can read back.
Automations: retry and delay
Any network action (http_request, send_webhook, send_email, send_whatsapp) takes config.retry: { attempts, delaySeconds }, up to five tries. A delay action with { minutes | hours | days } pauses the run and resumes the actions after it on time, so “remind the customer 24 hours later” is one automation, not a scheduler.
Rate limit
600 requests per minute per app key, with a burst allowance of 200 so a screen that fires a dozen calls at once is not penalised. Calls without a key get 60 per minute per IP. Every response carries X-RateLimit-Limit and X-RateLimit-Remaining; over the limit you get 429 with a Retry-After header. The limit is per key, not per IP, so one server can serve all your users.
From backend to a URL
Hosting a frontend
TaskLite hosts static frontends. deploy_frontend puts a site live at https://{slug}.tasklite.dev with HTTPS, keeps every version, and needs no server, hosting account or CI on your side. The app is published on the first deploy.
Three ways to hand over the site
Exactly one of them per call. Which one depends on where the files are, not on what the site is.
| Input | When | Limits |
|---|---|---|
files | The assistant wrote the page in the conversation: ChatGPT, Claude, any hosted client. Pass each file as { path, content }; binaries with encoding: "base64". Must include index.html. | 500 files, 8MB decoded |
zipUrl | A build already exists somewhere public: a Lovable or Bolt export, a GitHub release asset. https only. A zip whose only top-level entry is a folder is re-rooted. | 50MB |
dir | The MCP runs on the machine with the build output (Claude Code, Cursor, a local install). Pass the folder that contains index.html: dist/, build/, out/. | 50MB zipped |
From ChatGPT, in one turn
deploy_frontend(
appId: "app-xxxxxx",
files: [
{ path: "index.html", content: "<!doctype html>…<script src=\"app.js\"></script>" },
{ path: "app.js", content: "fetch('/api/orders').then(r => r.json()).then(render)" }
]
)
→ { version: 1,
url: "https://bike-repair-api.tasklite.dev",
previewUrl: "https://v1--bike-repair-api.tasklite.dev",
fileCount: 2 }Calling the backend from the page
Hosted pages call the app API at the relative path /api/{endpoint}. The hosting proxy attaches the app identity server-side, so there is no base URL to configure, no CORS, and no API key in browser code. Generated frontends should be told exactly that: relative /api, no key.
Versions, previews and rollback
Every deploy is a numbered version, and each one keeps an address of its own: https://v{n}--{slug}.tasklite.dev serves that exact release, straight from its frozen directory, without changing what the live URL shows. Open one to check what a build looked like, or hand the link to someone before you decide to restore it. Previews are public to anyone with the link and carry noindex, nofollow, so an old build never competes with the live site in search.
list_deployments shows every version, which one is live, and each version's previewUrl. Only the last five bundles stay on disk: an older version reports onDisk: false and has no preview, it cannot be restored either. rollback_deployment points the live URL at any version still on disk. A custom domain is not offered yet: every address is under tasklite.dev.
Diagnostics
Reading a failure
Every genuine authentication or publishing failure returns 401 with a message naming the cause. That single fact resolves most confusion: if you did not get a 4xx, you did not reach the API.
| What you saw | What it means | Do this |
|---|---|---|
200 text/html | You called the admin host, not the API. The page you received is the admin interface answering an unknown path. | Point the client at api.tasklite.net. |
401 application/json | You reached the API. The message names the exact cause: missing key, invalid key, unpublished app, or unknown endpoint. | Read the message field and fix that one thing. |
| 404 on the admin host | The guard on app.tasklite.net, telling you the API lives elsewhere. | Same fix: switch hosts. |
| 400 exposes no writable columns | The endpoint has no exposedColumns, so every field you sent would have been discarded. | update_app_endpoint with the column ids. |
| 200 rows, no fields | Same cause, read side. Only item metadata is exposed. | list_app_endpoints, an endpoint showing 0 columns is broken. |
| 404 App not found | No app with that id or slug in this organization. | list_apps to see what actually exists. |
| 403 not enabled on endpoint | A deliberate configuration, not a fault: the endpoint’s allowedMethods omit that verb. New endpoints default to GET alone. | update_app_endpoint to add it: or, for deletes, use the admin, which is what most apps should do. |
| 400 text arrived corrupted | The body contains U+FFFD, so the encoding broke before the request was sent and those characters are already lost. | Send the body as UTF-8. On Windows shells this is the console codepage, not TaskLite. |
| 429 | Rate limited. | Honour Retry-After; batch server-side. |
A 200 is the failure worth learning to distrust. Both of the silent modes above answer successfully, one with a web page, one with rows stripped of every field you care about. Check the content-type before you debug anything else.
48 tools · @tasklite/mcp 0.14.3
Tool reference
Grouped in the order you reach for them; names, descriptions and parameters are read from the connector itself at build time, so this list is exactly what the server declares. Open a tool's parameter line for the full schema. Tools marked destructive are worth confirming with a human before calling. The 5 tools marked local install only are the account tools, the hosted connector authenticates with OAuth instead. Everything else, hosting included, works from ChatGPT, Claude and every hosted client.
Account & connection
- connection_statusread-onlylocal install only
- Whether this machine is connected. Call it first when anything fails with an auth error.
- sign_uplocal install only
- Create a new account and organization from the conversation. Local install only; hosted connections use OAuth.
email · name · organizationName
- email * string
- Email address
- name * string
- The user's full name
- organizationName * string
- Business/organization name
- connectlocal install only
- Connect with a personal API key (
tl_…), or switch accounts.apiKey
- apiKey * string
- Personal TaskLite API key, starts with tl_
- loginlocal install only
- Connect with email and password. Creates and stores a key, so the password is used once.
email · password
- email * string
- Email address
- password * string
- Used once to mint an API key; never stored
- disconnectdestructivelocal install only
- Forget the stored credential. Deletes nothing server-side and does not revoke the key.
- list_organizationsread-only
- Organizations you belong to. The id feeds every other tool's
organizationId. - configure_external_access
- Read or change how external users get into an organization:
registrationPolicyopen or approval, and the app login URL. Reads when called without fields.organizationId? · registrationPolicy? · appLoginUrl?
- organizationId string
- Defaults to the credential organization
- registrationPolicy "closed" | "approval" | "open"
- How external sign-ups are admitted: open, approval or closed
- appLoginUrl string
- https URL of your app's login page for external users; "" clears it
Modelling
- list_projectsread-only
- Projects in an organization.
organizationId? · page? · limit?
- organizationId string
- Defaults to the credential organization
- page number
- 1-based; defaults to 1
- limit number
- Defaults to 50
- list_boardsread-only
- Boards inside a project: id, name, description. The only way to discover a
boardIdwithout being handed a URL.projectId · page? · limit?
- projectId * string
- Project id (from list_projects / create_project)
- page number
- 1-based; defaults to 1
- limit number
- Defaults to 50
- build_backend
- One call: the project, its boards (plain data tables by default), typed columns, relations between the boards, optional sample rows (relation cells by the related row's title), and optionally a published API with an endpoint per board and a server-side key. The model designs the schema, the tool executes it and returns one summary; API field names are derived from column names and never collide with reserved ones. Start here when the user describes a whole system.
project · boards · api? · organizationId?
- project * object
- The project that holds the boards
- boards * object[]
- The boards (tables) of the backend, in any order
- api object
- Include to publish a REST API over every board and mint a key; omit for a boards-only build
- organizationId string
- Organization id; needed only when the account belongs to several (the error then lists them)
- create_project
- A project is one business process. Boards live inside it.
name · description? · organizationId?
- name * string
- Human-readable name
- description string
- Free-text description
- organizationId string
- Organization id; defaults to the credential organization when omitted
- create_board
- A board is a data table.
kind: "tasks"(default) adds the built-in task columns;kind: "data"is a plain table with only the columns you add: customers, orders, products. Add columns next.projectId · name · description? · kind?
- projectId * string
- Project id (from list_projects / create_project)
- name * string
- Human-readable name
- description string
- Free-text description
- kind "tasks" | "data"
- "tasks": with the built-in task columns (status, priority, assignee, due date, tags). "data": only the columns you add. Default tasks.
- create_column
- A typed column. text, rich_text, number, status, date, datetime, duration, people, checkbox, dropdown, label, priority, link, email, phone, relation, lookup, rollup, rating, currency, file. Rules live in
settings.validation: unique, min, max, minLength, maxLength, pattern.projectId · boardId · name · type · settings? · isRequired? · force?
- projectId * string
- Project id (from list_projects / create_project)
- boardId * string
- Board id (from list_boards / create_board)
- name * string
- Human-readable name
- type * string
- Column type: text, rich_text, number, status, date, datetime, duration, people, checkbox, dropdown, label, priority, link, email, phone, relation, lookup, rollup, rating, currency, file
- settings object
- Type-specific settings. For dropdown/status/priority: options, either as labels ["A","B"] or as full objects [{value,label,color}], labels are expanded server-side, and colors are assigned if you do not supply them.
- isRequired boolean
- Require a non-blank value on every App API create
- force boolean
- Create the column even when the name suggests a different type
- get_board_schemaread-only
- The board with its full column schema: ids, names, types, settings. Required before writing cells.
projectId · boardId
- projectId * string
- Project id (from list_projects / create_project)
- boardId * string
- Board id (from list_boards / create_board)
- update_column
- Change a column after the fact, rename, change type (
number→currency), replace dropdown options, toggle required or hidden. A type change converts the values that convert and clears the rest, and says how many of each.projectId · boardId · columnId · name? · type? · settings? · isRequired? · isHidden? · description? · force?
- projectId * string
- Project id (from list_projects / create_project)
- boardId * string
- Board id (from list_boards / create_board)
- columnId * string
- Column id (from get_board_schema)
- name string
- Human-readable name
- type string
- New column type (same list as create_column)
- settings object
- Replaces the column settings, e.g. { options: [...] } for dropdown/status
- isRequired boolean
- Require a non-blank value on every App API create
- isHidden boolean
- Hide the column in the TaskLite UI
- description string
- Free-text description
- force boolean
- Skip the name/type sanity check
- delete_columndestructive
- Remove a column and everything in it. Destructive.
projectId · boardId · columnId
- projectId * string
- Project id (from list_projects / create_project)
- boardId * string
- Board id (from list_boards / create_board)
- columnId * string
- Column id (from get_board_schema)
- reorder_columns
- Set the column order from a full list of column ids.
projectId · boardId · columnIds
- projectId * string
- Project id (from list_projects / create_project)
- boardId * string
- Board id (from list_boards / create_board)
- columnIds * string[]
- Column ids in the new order (every column of the board)
- update_board
- Rename a board or change its description.
projectId · boardId · name? · description?
- projectId * string
- Project id (from list_projects / create_project)
- boardId * string
- Board id (from list_boards / create_board)
- name string
- Human-readable name
- description string
- Free-text description
- delete_boarddestructive
- Delete a board with all its items. Destructive.
projectId · boardId
- projectId * string
- Project id (from list_projects / create_project)
- boardId * string
- Board id (from list_boards / create_board)
- delete_projectdestructive
- Delete a project with every board, column and row in it. Destructive; it lands in the organization recycle bin, so an admin can restore it.
projectId · organizationId?
- projectId * string
- Project id (from list_projects / create_project)
- organizationId string
- Organization id; defaults to the credential organization when omitted
Data
- searchread-only
- Full-text search across projects, boards and items in the organization. Returns
{ results: [{ id, title, url }] }, the shape ChatGPT connectors and deep research expect; pass an id tofetch.query · projectId? · limit?
- query * string
- Search text
- projectId string
- Limit the search to one project
- limit integer
- Max results, default 20
- fetchread-only
- One project, board or item in full by the id
searchreturned (or an app URL path). Returns{ id, title, text, url, metadata }.id · organizationId?
- id * string
- An id from search, or an app URL path such as /projects/…/boards/…/items/…
- organizationId string
- Only needed for project ids when the credential has no default organization; otherwise resolved automatically
- query_itemsread-only
- Rows with their cell values. Pages through automatically when unpaged.
projectId · boardId · search? · status? · excludeStatus? · priority? · sort? · archived? · limit? · page?
- projectId * string
- Project id (from list_projects / create_project)
- boardId * string
- Board id (from list_boards / create_board)
- search string
- Free text; matches the row title and its text cells
- status string
- Only rows whose status is one of these, comma separated. Matched against the board's status column by option value or label, case-insensitive: "todo,in_progress" on a task board, "Received,In Repair" on a data board whose status column lists those (get_board_schema shows the options)
- excludeStatus string
- Only rows whose status is NOT one of these, comma separated, resolved like status. The way to ask for everything that is not done: excludeStatus "done" (task board) or "Completed" (a data board with that option). Rows with no status at all are kept
- priority string
- Only rows with one of these priorities, comma separated (task boards only)
- sort string
- Sort by title, createdAt, updatedAt or status; prefix with "-" for descending, e.g. "-createdAt" for newest first. Anything else keeps the board order
- archived "active" | "archived" | "all"
- Which rows to include. Default active
- limit integer
- Page size; omit to fetch all items
- page integer
- 1-based page, only with limit
- export_projectread-only
- The whole project as JSON: boards, columns, items and cells.
GET …/projects/{id}/export.jsonreturns it uncapped.projectId · organizationId? · maxItemsPerBoard?
- projectId * string
- Project id (from list_projects / create_project)
- organizationId string
- Defaults to the credential organization
- maxItemsPerBoard integer
- Default 200
- create_item
- Create a row with all of its cells in one call,
cellsmaps column id to value.set_cellis for later edits.projectId · boardId · title · description? · status? · priority? · dueDate? · tags? · cells?
- projectId * string
- Project id (from list_projects / create_project)
- boardId * string
- Board id (from list_boards / create_board)
- title * string
- Item title, shown as the row name
- description string
- Free-text description
- status string
- Status value (todo, in_progress, done, or a value from the board's status options)
- priority string
- Priority: low, medium, high or urgent
- dueDate string
- ISO date
- tags string[]
- Tags as an array of strings
- cells object
- Cell values keyed by column id: { "<columnId>": value }. Scalars, { amount, currency } for currency, { relatedItemIds: [...] } for relations
- update_item
- Update title, description, status, priority, dueDate, tags.
projectId · boardId · itemId · title? · description? · status? · priority? · dueDate? · tags?
- projectId * string
- Project id (from list_projects / create_project)
- boardId * string
- Board id (from list_boards / create_board)
- itemId * string
- Item (row) id
- title string
- Item title, shown as the row name
- description string
- Free-text description
- status string
- Status value (todo, in_progress, done, or a value from the board's status options)
- priority string
- Priority: low, medium, high or urgent
- dueDate string
- Due date, ISO 8601 (YYYY-MM-DD or full timestamp)
- tags string[]
- Tags as an array of strings
- set_cell
- Set one cell by
columnId. The reliable way to write column data.projectId · boardId · itemId · columnId · value
- projectId * string
- Project id (from list_projects / create_project)
- boardId * string
- Board id (from list_boards / create_board)
- itemId * string
- Item (row) id
- columnId * string
- Column id (from get_board_schema)
- value * string | number | boolean | null | any[] | object
- The new cell value; shape depends on the column type
- delete_itemdestructive
- Delete a row. Destructive.
projectId · boardId · itemId
- projectId * string
- Project id (from list_projects / create_project)
- boardId * string
- Board id (from list_boards / create_board)
- itemId * string
- Item id (from query_items / create_item)
Threads
- list_commentsread-only
- An item’s correspondence thread, oldest first, with authors and mentions.
projectId · itemId · page? · limit?
- projectId * string
- Project id (from list_projects / create_project)
- itemId * string
- Item (row) id
- page integer
- 1-based page, default 1
- limit integer
- Page size, default 20
- add_comment
- Post to the thread.
mentionedUserIdsnotifies people;attachmentIdsreferences uploaded files.projectId · itemId · content · mentionedUserIds? · attachmentIds?
- projectId * string
- Project id (from list_projects / create_project)
- itemId * string
- Item (row) id
- content * string
- The comment text
- mentionedUserIds string[]
- User ids to @mention and notify
- attachmentIds string[]
- Ids of already-uploaded attachments to link
- update_comment
- Edit a comment. Author only.
projectId · itemId · commentId · content
- projectId * string
- Project id (from list_projects / create_project)
- itemId * string
- Item (row) id
- commentId * string
- Comment id (from list_comments)
- content * string
- The new comment text
- delete_commentdestructive
- Remove a comment. Destructive.
projectId · itemId · commentId
- projectId * string
- Project id (from list_projects / create_project)
- itemId * string
- Item id (from query_items / create_item)
- commentId * string
- Comment id (from list_comments / add_comment)
Apps & the REST surface
- create_app
- A named API surface over a project’s boards, for an external frontend.
name · projectId · organizationId?
- name * string
- Human-readable name
- projectId * string
- Project id (from list_projects / create_project)
- organizationId string
- Organization id; defaults to the credential organization when omitted
- publish_app
- Required before endpoints accept external calls.
appId · organizationId?
- appId * string
- App id or slug (from list_apps / create_app)
- organizationId string
- Organization id; defaults to the credential organization when omitted
- list_appsread-only
- Apps in the organization with id, slug and status. This is how you map a slug back to its app.
organizationId?
- organizationId string
- Organization id; defaults to the credential organization when omitted
- create_app_endpoint
- Expose a board at
/apps/{appSlug}/api/{slug}. PassexposedColumns; without it the endpoint is inert.appId · boardId · slug · name · allowedMethods? · exposedColumns? · rowLevelSecurity? · organizationId?
- appId * string
- App id or slug (from list_apps / create_app)
- boardId * string
- Board id (from list_boards / create_board)
- slug * string
- URL slug: lowercase letters, digits and dashes
- name * string
- Human-readable name
- allowedMethods "GET" | "POST" | "PATCH" | "DELETE"[]
- HTTP methods the endpoint accepts: GET, POST, PATCH, DELETE
- exposedColumns object[]
- Columns the endpoint reads and writes, with the JSON key each one gets; without it the endpoint returns bare metadata
- rowLevelSecurity object
- Row-level security: when enabled, each app user sees and edits only their own rows
- organizationId string
- Organization id; defaults to the credential organization when omitted
- list_app_endpointsread-only
- Endpoints with their methods and exposed-column counts. A count of 0 means broken.
appId · organizationId?
- appId * string
- App id or slug (from list_apps / create_app)
- organizationId string
- Organization id; defaults to the credential organization when omitted
- update_app_endpoint
- Change an endpoint, most often to add
exposedColumnsto one created without them.appId · endpointId · slug? · name? · allowedMethods? · exposedColumns? · isActive? · organizationId?
- appId * string
- App id or slug (from list_apps / create_app)
- endpointId * string
- Endpoint id (from list_app_endpoints)
- slug string
- URL slug: lowercase letters, digits and dashes
- name string
- Human-readable name
- allowedMethods "GET" | "POST" | "PATCH" | "DELETE"[]
- HTTP methods the endpoint accepts: GET, POST, PATCH, DELETE
- exposedColumns object[]
- Replacement list of exposed columns (same shape as create_app_endpoint)
- isActive boolean
- Whether it is active
- organizationId string
- Organization id; defaults to the credential organization when omitted
- create_app_api_key
- An app key (
tk_…). Server-side only, never in browser code.appId · name? · scopes? · organizationId?
- appId * string
- App id or slug (from list_apps / create_app)
- name string
- Human-readable name
- scopes "read" | "write"[]
- Permissions recorded on the key: ["read"] or ["read","write"]. Omit to match the app: write when any endpoint accepts POST, PATCH or DELETE.
- organizationId string
- Organization id; defaults to the credential organization when omitted
- push_statusread-only
- Whether this app can notify phones, and how many devices are registered.
appId · organizationId?
- appId * string
- App id or slug (from list_apps / create_app)
- organizationId string
- Organization id; defaults to the credential organization when omitted
- send_test_push
- One real notification, to prove the chain before an automation depends on it.
appId · userIds · title? · body? · organizationId?
- appId * string
- App id or slug (from list_apps / create_app)
- userIds * string[]
- App user ids to notify (the same ids row-level security uses)
- title string
- Notification title; defaults to "TaskLite"
- body string
- Notification body
- organizationId string
- Organization id; defaults to the credential organization when omitted
- get_app_specread-only
- Machine-readable spec: absolute base URL, endpoints, methods, fields. Accepts a UUID or a slug.
appId · format? · organizationId?
- appId * string
- App id or slug (from list_apps / create_app)
- format "tasklite" | "openapi"
- "tasklite" (default): compact endpoint list. "openapi": OpenAPI 3.1 document, every endpoint with typed fields, filter grammar, auth and errors
- organizationId string
- Organization id; defaults to the credential organization when omitted
- get_frontend_promptread-only
- A ready-made description of the backend for pasting into v0, bolt, lovable or cursor.
appId · tool · organizationId?
- appId * string
- App id or slug (from list_apps / create_app)
- tool * "v0" | "bolt" | "lovable" | "cursor" | "claude-code"
- Target tool the prompt is written for
- organizationId string
- Organization id; defaults to the credential organization when omitted
Frontend hosting
- deploy_frontend
- Put a static frontend live at
{slug}.tasklite.dev. Hand it over asfiles(inline, the path from ChatGPT and every hosted client), azipUrl(a Lovable/Bolt export or a GitHub release), or a localdir. Pages call the app API at the relative/api/…; the proxy adds the app identity, so no key reaches the browser.appId · files? · zipUrl? · dir? · organizationId?
- appId * string
- App UUID or slug (app-xxxxxx), see list_apps
- files object[]
- The site files inline. Must include index.html. Up to 500 files / 8MB decoded, right for a frontend written in the conversation
- zipUrl string
- Public https URL of a zip of the BUILD OUTPUT (index.html at the root, or inside a single top-level folder). Up to 50MB
- dir string
- Local path to the BUILD OUTPUT directory (the one containing index.html), not the project root. Only where the MCP runs on the same machine as the files
- organizationId string
- Organization id; defaults to the credential organization when omitted
- list_deploymentsread-only
- Versions of an app’s hosted frontend, which one is live, and the public URL.
appId · organizationId?
- appId * string
- App id or slug (from list_apps / create_app)
- organizationId string
- Organization id; defaults to the credential organization when omitted
- rollback_deployment
- Point the live URL back at a previous version from
list_deployments. The last five versions are kept, and each one is also readable on its own atv{n}--{slug}.tasklite.devwithout changing what is live.appId · version · organizationId?
- appId * string
- App id or slug (from list_apps / create_app)
- version * integer
- Deployment version number (from list_deployments)
- organizationId string
- Organization id; defaults to the credential organization when omitted
Automation
- create_automation
- When something happens, do something.
http_requestcalls an external API and writes the answer back into columns, it needsresponseMapping: [{ path, columnId }]or it runs and stores nothing. Adelayaction pauses the run; network actions takeconfig.retry.projectId · boardId · name · trigger · triggerConfig? · actions · isActive?
- projectId * string
- Project id (from list_projects / create_project)
- boardId * string
- Board id (from list_boards / create_board)
- name * string
- Human-readable name
- trigger * "item_created" | "status_changed" | "column_value_changed" | "date_approaching" | "scheduled"
- Event that starts the automation: item_created, status_changed, column_value_changed, date_approaching, or scheduled (cron)
- triggerConfig object
- e.g. { cron: "0 8 * * *" } for scheduled, { columnName } for column_value_changed
- actions * object[]
- e.g. [{ type: "http_request", config: { url: "https://api.frankfurter.app/latest?from=USD&to=ILS", method: "GET", responseMapping: [{ path: "rates.ILS", columnId: "<column id from get_board_schema>" }] } }]
- isActive boolean
- Whether it is active
- list_automationsread-only
- What already runs on a board, before you add another.
projectId · boardId
- projectId * string
- Project id (from list_projects / create_project)
- boardId * string
- Board id (from list_boards / create_board)
Learned the hard way
Known traps
The first column mirrors the title, until you write to it
On a board without a column literally named Title, the first custom column shows the item title so the grid has something to display. Once you store your own value there (a phone, a name), it is yours: renaming the item no longer touches it. If you want the title visible in the grid, give the board a Title column or put a free-text column first.
Reserved aliases are refused
An exposed column cannot be aliased status, title, priority, dueDate or another row field - create_app_endpoint answers 400 and names the clash. Pick quoteStatus, deliveryStatus. Duplicate aliases and aliases that are not valid identifiers are refused the same way.
Without an alias, the key is the column name
An exposed column with no alias is keyed by the column name on reads and writes alike, whether or not the row has a cell yet (the raw column id is still accepted on write). Rename the column and the key changes with it, so set an alias when client code depends on the key.
Endpoints are live immediately
New endpoints added after publish_app work at once. There is no republish step, and reaching for one is usually a sign the real problem is elsewhere.
Column type names are not the obvious ones
A single-select is dropdown, not select. A person field is people, not person.