taskadence: the TasKadence API for Python
TasKadence is task and project management for teams. This package lets your Python code
read and change your organization's tasks, projects and teams, and it adds tm, a command line for the same things.
0.xis a pre-release. Any0.xrelease may change anything. Pin an exact version and read the changelog before upgrading.
Documentation: docs.taskadence.com · Source: github.com/indrasol/taskadence-python · Issues: GitHub issues
Get started in 3 steps
You need Python 3.10 or later and a TasKadence account. This takes about five minutes.
1. Get a token
A token is a password for programs: it lets this package act for you in one organization.
- In TasKadence, open Developers in the sidebar, stay on the Tokens tab and click Create token.
- Give it a name you will recognise later (for example "My laptop").
- Under Scopes, tick what the token may do:
- To read (enough for this guide): Read on the Tasks row. Add Read on Projects and Teams if you will read those too.
- To create and change things: Write on the same rows. Write includes Read.
- Click Create token and copy it (it starts with
tkd_live_). It is shown once. If you lose it, create a new one and revoke the old one.
2. Install
pip install taskadence # the Python client
pip install "taskadence[cli]" # the client plus the `tm` command line
3. Make your first call
Put the token in the TASKADENCE_TOKEN environment variable, in the terminal you will run Python from.
macOS or Linux:
export TASKADENCE_TOKEN="tkd_live_paste_your_token_here"
Windows PowerShell:
$env:TASKADENCE_TOKEN = "tkd_live_paste_your_token_here"
Save this as hello.py and run python hello.py:
from taskadence import Taskadence
tm = Taskadence() # reads TASKADENCE_TOKEN from the environment
org = tm.me().organizations[0] # the organization your token belongs to
print(f"Organization: {org.name} ({org.org_id})")
for task in tm.tasks.list(org_id=org.org_id, limit=5).data: # your first 5 tasks
print(task.task_id, task.status, task.title)
You should see your organization, then up to five tasks (yours will have different names and ids):
Organization: Acme Inc (O123456)
T123456 in_progress Write the launch announcement
T123457 not_started Book the venue
T123458 completed Draft the budget
No tasks yet? Only the first line prints. Create one in the app, or see Create and change tasks below.
The same with the command line (it reads the same TASKADENCE_TOKEN):
tm me
tm tasks list --limit 5
ada (user) <ada@example.com>
Organizations
┏━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┓
┃ org_id ┃ name ┃ role ┃ designation ┃
┡━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━┩
│ O123456 │ Acme Inc │ owner │ Engineering Manager │
└─────────┴──────────┴───────┴─────────────────────┘
1 row
tm tasks list prints a table of task_id, title, status, priority, assignee, due_date and project_id.
Prefer not to keep the token in an environment variable? tm auth login asks for it once and stores it in your
operating system's keyring.
Something went wrong? See Troubleshooting near the end of this page.
Key ideas
Four things that make the rest of this page easier to read.
- Organization. Everything in TasKadence (tasks, projects, teams) belongs to an organization, your company's
workspace. Most calls need its id,
org_id. A token works in exactly one organization. - Token and scopes. The token proves who you are; its scopes say what it may do.
tasks:readreads tasks,tasks:writealso creates and changes them, and the same pattern holds forprojects,teamsandwebhooks. A call your token's scopes do not cover fails with a clear error naming the scope it needs. Each method's docstring names its scope too. A test token (tkd_test_…) can only read. Scopes are fixed when the token is created: to add one, create a new token. All scopes are listed in Authentication. - IDs. Every object has a short id with a letter in front: tasks
T123456, projectsP123456, organizationsO123456, saved viewsV123456, webhooksWH123456. Find them withtm.me()(your organization), with any list (tm.tasks.list(...),tm tasks list,tm projects list), or in the app's address bar: a task's page ishttps://taskadence.com/tasks/T123456, andorg_id=O123456in the address names the organization. - Pre-release. Versions
0.xmay change anything between releases, so pin one (pip install taskadence==0.1.0). From 1.0 on, the SDK follows semantic versioning.
Using the client
Every snippet below runs as it is after step 3. They start the same way: make a client and look up your organization's id.
Create and change tasks
You create, read, change and delete tasks with tm.tasks; every other kind of object works the same way.
import datetime as dt
from taskadence import Taskadence
from taskadence.models import TaskCreate, TaskUpdate
tm = Taskadence()
org_id = tm.me().organizations[0].org_id
task = tm.tasks.create(TaskCreate(org_id=org_id, title="Try the TasKadence SDK", priority="high",
due_date=dt.date(2027, 1, 15)))
print("created", task.task_id)
tm.tasks.update(task.task_id, TaskUpdate(status="in_progress")) # changes only the fields you set
tm.tasks.update(task.task_id, {"due_date": None}) # None clears a field; a plain dict works too
print(tm.tasks.get(task.task_id).status) # in_progress
tm.tasks.delete(task.task_id) # tidy up
Needs tasks:write. update sends only the fields you pass and leaves the others alone (the API calls this a JSON
merge-patch). tm.tasks.replace (PUT) replaces the whole task instead.
There is a method for every public operation of the API, named tm.<resource>.<verb>: tasks.create, tasks.list,
tasks.get (also read), tasks.update, tasks.replace, tasks.delete, tasks.move (also set_project),
projects.list, views.rows, webhooks.test, and so on. The full list is in the
API reference. Every answer is a typed
model from taskadence.models with to_dict() and from_dict().
Lists and pagination
A list can be long, so the API sends it in pages; looping over the result fetches every page for you.
from taskadence import Taskadence
tm = Taskadence()
org_id = tm.me().organizations[0].org_id
page = tm.tasks.list(org_id=org_id, filter={"status": ["in_progress", "blocked"]}, sort_by="due_date", limit=100)
print(len(page.data), "on the first page; more follow:", page.next_cursor is not None)
for task in page: # every matching task, page after page
print(task.task_id, task.title)
for p in page.pages(): # or page by page
print(len(p.data))
filter={key: value} narrows the list (a list of values means "any of these"); an unknown key raises ValueError
naming the allowed ones. A page may be short, or even empty, while more follow; the loop handles it. page.envelope
is the full answer, for the lists that carry totals beside data.
Errors
When the API says no, you get an exception that says why, with the request_id support needs to find it.
from taskadence import InsufficientScopeError, NotFoundError, Taskadence, TaskadenceError
tm = Taskadence()
try:
tm.tasks.get("T000000") # an id that does not exist
except NotFoundError as exc:
print("not found:", exc.detail)
except InsufficientScopeError as exc:
print("this token needs", exc.required_scope) # for example tasks:read
except TaskadenceError as exc: # every other API error
print(exc.status, exc.detail, "request_id:", exc.request_id)
Every error is a subclass of taskadence.TaskadenceError with .status, .type, .title, .detail, .request_id
and .errors:
| Status | Exceptions |
|---|---|
| 400 | BadRequestError · InvalidParameterError · IdempotencyKeyInvalidError |
| 401 | AuthenticationError · TokenInvalidError · TokenExpiredError · TokenRevokedError |
| 403 | ForbiddenError · InsufficientScopeError · TestTokenReadOnlyError · TokenPolicyError (also 422) |
| 404 · 409 · 412 | NotFoundError · ConflictError (IdempotencyKeyInFlightError) · PreconditionFailedError |
| 422 | UnprocessableEntityError · ValidationError (InvalidValueError) · IdempotencyKeyReusedError · UrlRefusedError |
| 429 · 5xx | RateLimitedError (.retry_after) · InternalError |
| none | APIConnectionError / APITimeoutError (no answer from the server) · ResponseValidationError (an answer this SDK version cannot read: upgrade) |
The API sends every error as application/problem+json; see
Errors for each type.
Uploading files
Attach files to tasks and projects: up to 100 MB each, sent straight to storage, with a progress callback if you want one.
from pathlib import Path
from taskadence import Taskadence
tm = Taskadence()
org_id = tm.me().organizations[0].org_id
Path("notes.txt").write_text("Hello from the SDK\n")
task = tm.tasks.create({"org_id": org_id, "title": "Has an attachment"})
tm.task_attachments.create(task_id=task.task_id, file="notes.txt", title="Notes", # a path, bytes or an open file
progress=lambda sent, total: print(f"{sent * 100 // total}%"))
print(len(tm.task_attachments.list(task_id=task.task_id).data), "attachment")
tm.tasks.delete(task.task_id)
Needs tasks:write. tm.project_resources.upload(project_id=..., file=...) does the same for a project (with
projects:write). How uploads work: Uploading files.
pandas
Turn any list into a pandas DataFrame, one row per item, for analysis or a CSV. Needs pip install "taskadence[pandas]".
from taskadence import Taskadence
tm = Taskadence()
org_id = tm.me().organizations[0].org_id
frame = tm.tasks.list(org_id=org_id).to_dataframe() # every page; to_dataframe(all_pages=False) for one
print(frame[["task_id", "title", "status"]].head())
Columns keep the API's field names: a nested object becomes dotted columns (type_data.severity), a list becomes one
comma-separated string, *_at columns are timezone-aware datetimes and *_date columns are dates. A saved view
works the same way: tm.views.rows("V123456").to_dataframe(). Without the extra installed you get an ImportError
saying so.
Async
For asyncio code, AsyncTaskadence has the same methods; you await them.
import asyncio
from taskadence import AsyncTaskadence
async def main() -> None:
async with AsyncTaskadence() as tm:
org_id = (await tm.me()).organizations[0].org_id
async for task in await tm.tasks.list(org_id=org_id, filter={"status": ["blocked"]}):
print(task.task_id, task.title)
asyncio.run(main())
Webhooks
A webhook is TasKadence calling your server when something changes (a task is created, a comment is added). These two functions check that a call really came from TasKadence and read it.
from taskadence import webhooks
SECRET = "whsec_your_webhook_secret" # shown when you create the webhook
def receive(headers, raw_body: bytes) -> int:
"""Call this from your web framework's handler with the request's headers and its raw body."""
if not webhooks.verify(SECRET, headers, raw_body): # the RAW body, not re-serialized JSON
return 400
event = webhooks.parse(raw_body) # .id .type_ .org_id .data.resource_id …
print(event.type_, event.data.resource_id) # deliveries can repeat: skip an event.id you have seen
return 200
verify checks the signature (HMAC-SHA256 over id.timestamp.body, the Standard Webhooks scheme), refuses a call
older than 5 minutes (tolerance, in seconds), and accepts two secrets during the 24 hours after you rotate one
(verify([new, old], ...)). raise_on_failure=True raises WebhookVerificationError with the reason instead of
returning False. Events and payloads: Webhooks.
The command line
tm does the everyday things from a terminal. Install it with pip install "taskadence[cli]".
tm auth login # asks for the token (hidden), checks it, stores it
tm me # who you are, and your organization
tm tasks list --status in_progress --status blocked
tm tasks create --title "Try the TasKadence CLI" --due 2027-01-15
tm projects list
tm storage # how much file storage your organization uses
tm --version
Commands that take an id use T123456, P123456, V123456 and WH123456 below as placeholders: copy a real one
from tm tasks list, tm projects list, tm webhooks list or the app's address bar.
tm tasks get T123456 --json
tm tasks update T123456 --status completed # refuses to overwrite a change someone made since you read it
tm tasks attach T123456 ./report.pdf --title "Q3 report" # straight to storage, with a progress bar
tm projects upload P123456 ./plan.xlsx
tm views rows V123456 --csv > rows.csv
tm webhooks test WH123456
tm webhooks deliveries WH123456
tm tokens list
- Output: a table by default,
--jsonfor the raw objects. - Organization:
--orgfalls back toTASKADENCE_ORG, then to your organization when you have exactly one. - Token: from
TASKADENCE_TOKEN, else the OS keyring, else~/.config/taskadence/config.toml(mode 600, written only when no keyring is available or with--no-keyring).tmonly ever prints the token's first 12 characters. - Names:
tmandtaskadenceare the same command. - Exit codes: 0 success, 1 an API or connection error (printed with its
request_id), 2 a usage error.
AI assistants (MCP)
To let an AI assistant (Claude, Cursor, VS Code and others) work with your tasks, connect it to the TasKadence MCP server. If your assistant can add a server by URL, do that: it signs in with OAuth and needs no token. See Connect.
For assistants that can only start a local program, two small packages from this repository do the job, the same thing in two languages:
| Package | Run | Set-up guide |
|---|---|---|
taskadence-mcp (PyPI) |
uvx taskadence-mcp |
packages/taskadence-mcp |
@taskadence/mcp (npm) |
npx -y @taskadence/mcp |
packages/mcp-node |
Advanced
Most people never need this section. It explains what the SDK already does for you and how to tune it.
Configuration
Taskadence(token=None, base_url=None, *, timeout=30, max_retries=3, api_version=None, max_retry_after=60, headers=None, http_client=None). With no token, it reads TASKADENCE_TOKEN; with no base_url, it reads
TASKADENCE_API_URL (default https://api.taskadence.com). Use it as a context manager (with Taskadence() as tm:)
or call tm.close() when you are done.
Retries
What this means for you: a brief network blip, or the API asking you to slow down, does not crash your script. The SDK waits and tries again, when trying again is safe.
It retries up to max_retries times (default 3) on 429 (waiting as long as Retry-After says), 502, 503, 504 and
connection failures, waiting a little longer each time (exponential backoff with jitter). Safe means: GET, HEAD,
DELETE and PUT; PATCH only with if_match; POST only with an Idempotency-Key. A Retry-After longer than
max_retry_after seconds (default 60) is not waited out: the RateLimitedError is raised at once.
Idempotent creates
What this means for you: if the connection drops while you create something, the retry does not create a second copy.
Every create sends an Idempotency-Key (a fresh random id per call, reused by that call's retries), so the API
recognises a repeat. Pass your own with idempotency_key="..." (the API replays the first answer for 24 hours), or
idempotency_key=None for none (and then no retries).
ETags and conditional requests
What this means for you: you can say "save my change only if nobody changed this since I read it", so you never overwrite a teammate's edit by accident, and you can check cheaply whether something changed.
A model read by a single GET carries .etag, a fingerprint of that version:
from taskadence import NotModified, PreconditionFailedError, Taskadence
tm = Taskadence()
org_id = tm.me().organizations[0].org_id
task = tm.tasks.create({"org_id": org_id, "title": "ETag demo"})
read = tm.tasks.get(task.task_id) # read.etag: this version's fingerprint
tm.tasks.update(task.task_id, {"status": "in_progress"}, if_match=read.etag) # saved: nothing changed since the read
try:
tm.tasks.update(task.task_id, {"status": "blocked"}, if_match=read.etag) # the old fingerprint: refused (412)
except PreconditionFailedError:
print("it changed since we read it: read it again, then retry")
latest = tm.tasks.get(task.task_id)
print(tm.tasks.get(task.task_id, if_none_match=latest.etag) is NotModified) # True (304): unchanged, no body sent
tm.tasks.delete(task.task_id)
Deprecations
When the API marks an operation as going away (Deprecation and Sunset headers), the SDK emits one
DeprecationWarning per operation per process, naming the date and what replaces it. taskadence.API_VERSION names
the API version the SDK was built against (sent as the Taskadence-Version header).
Logging
The SDK logs to the taskadence logger at DEBUG level: one line per request (method, path, status, time, attempt,
request_id). It never logs the token or any header. An upload URL is logged without its query string.
import logging
logging.basicConfig()
logging.getLogger("taskadence").setLevel(logging.DEBUG)
OAuth apps
If you are building an app that other TasKadence users sign in to, run the OAuth 2.1 authorization flow
(/oauth/authorize, /oauth/token) with any OAuth library; this SDK does not wrap it. The access token you get works
like any other: Taskadence(token=...).
Troubleshooting
Most problems are the token, its scopes or an id. Every error carries a request_id; keep it if you ask for help.
- 401 (
AuthenticationError,TokenInvalidError,TokenExpiredError,TokenRevokedError): the token is wrong, expired or revoked. Check thatTASKADENCE_TOKENis set in the same terminal you run Python from (tm auth statusshows wheretmfinds it, and only its first 12 characters). If in doubt, create a new token in Developers > Tokens. - 403 insufficient scope (
InsufficientScopeError): the token is valid but may not do this.exc.required_scope(and the error message) names the scope it needs, for exampletasks:write. Scopes cannot be added to a token: create a new one with that scope ticked. Atkd_test_…token can never write (TestTokenReadOnlyError). - 404 (
NotFoundError): the id is wrong, belongs to another organization than your token's, or is in a project you cannot see. Copy the id again fromtm tasks listor the app's address bar. tm: command not found: install the extra,pip install "taskadence[cli]", in the Python you are using.- Finding the
request_id:exc.request_idon anyTaskadenceError;tmprints it with every error. - Still stuck? Open a GitHub issue with the
request_id, the SDK version (tm --version) and what you ran. Never paste a live token; its first 12 characters are enough.
Examples
examples/quickstart.py: a full round trip (create, conditional update, filtered list, DataFrame, delete) that leaves nothing behind.examples/streamlit_dashboard: an organization dashboard that needs nothing butTASKADENCE_TOKEN.
Support and security
- Questions and bugs: GitHub issues. Please never paste a live
token; a prefix (
tkd_live_ab12…) is enough. - Vulnerabilities: report them privately, as described in SECURITY.md (GitHub private vulnerability reporting, or srvcs.infra@indrasol.com).
- Contributing: see CONTRIBUTING.md.
License
Apache License 2.0. See LICENSE and NOTICE. The license grants no right to use the TasKadence or Indrasol names or marks.
Metadata
Release files for taskadence 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| taskadence-0.1.0.tar.gz | 450.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| taskadence-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 1.3 MB
Release files / taskadence-0.1.0.tar.gz
| Download URL | taskadence-0.1.0.tar.gz |
|---|---|
| Size | 450.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
763d88b1c5593e66ee6411032ced0c8514dd66da5cad79a7002c4b9c108cf5b9
|
|
BLAKE2b-256 checksum How to use checksums |
12c46e8a54a44c1fb8b8546aadd373e01149bcb64b52de1ba7045192b188814e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.
Transparency logRelease files / taskadence-0.1.0-py3-none-any.whl
| Download URL | taskadence-0.1.0-py3-none-any.whl |
|---|---|
| Size | 827.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d3a572c3f1548dd917dfc35e9409dc4ab28a3c1406433ed03fc39cf3f0a0fc64
|
|
BLAKE2b-256 checksum How to use checksums |
35c8fbeb5cbfec8bbc42963dca37c14b4233a70f059367a7838d3728d7b3c197
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.
Transparency log