Skip to content

Commit 1670cb0

Browse files
authored
Merge pull request #19 from taskbadger/docs/parent-nesting-v2.5.0
Docs for v2.5.0, v2.5.1 and v2.5.2
2 parents 93739ef + f05371b commit 1670cb0

7 files changed

Lines changed: 439 additions & 8 deletions

File tree

docs/changelog.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,34 @@ hide:
77

88
Full release notes for the Python SDK are available on [GitHub](https://github.com/taskbadger/taskbadger-python/releases).
99

10+
## v2.5.2
11+
12+
**2026-08-07**
13+
14+
**Python SDK**
15+
16+
* **FIX** [`taskbadger_track=False`](python-celery.md#opting-out) now opts a single Celery execution out of tracking, as an `apply_async` argument or in the message headers. Previously it could only ever enable tracking: it was ignored when the `CelerySystemIntegration` was auto-tracking, and `apply_async` overwrote it on tasks using `base=Task`.
17+
18+
## v2.5.1
19+
20+
**2026-08-07**
21+
22+
**Python SDK**
23+
24+
* **CHANGED** [`list_tasks`](python.md#listing-tasks) returns a `TaskList` of `taskbadger.Task` objects, which can be iterated over directly. It previously returned the generated `PaginatedTaskList`, whose `results` were internal models without the SDK's update methods. Note that an empty `TaskList` is falsy, where `PaginatedTaskList` was always truthy.
25+
* **FIX** Eager Celery tasks honour an explicit [`taskbadger_parent`](python-celery.md#subtasks), including `taskbadger_parent=None` to opt out of nesting.
26+
* **FIX** Copying or pickling a `Task` no longer recurses until the stack overflows.
27+
28+
## v2.5.0
29+
30+
**2026-08-06**
31+
32+
**Python SDK**
33+
34+
* **NEW** [Parent and child tasks](python.md#parent-and-child-tasks). Tasks can be nested one level deep via the [`parent`](data_model.md#parent) field on `create_task` / `update_task`, and `list_tasks` can filter by parent. The [`@track` decorator](python-decorator.md#nested-tasks), [Celery](python-celery.md#subtasks) and [Procrastinate](python-procrastinate.md#subtasks) integrations set it automatically for tasks enqueued from within a tracked task.
35+
* **NEW** [Context providers](python.md#error-context-providers), a pluggable way to attach extra data to a task when it errors, along with a [Sentry provider](python.md#sentry) that links a failed task to its Sentry issue. Install with `pip install 'taskbadger[sentry]'`.
36+
* **NEW** `Task.error` accepts an `exception` argument, which is passed to the configured context providers.
37+
1038
## v2.4.0
1139

1240
**2026-07-30**

docs/data_model.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ The main attributes or a task are:
4444
: This is a computed percentage which is equivalent to `100 * value / value_max`. This will be `null`
4545
if **value** is null.
4646

47+
<a name="data"></a>
4748
`data`
4849

4950
: This can be used to store arbitrary JSON data that may be useful to store along with the task such
@@ -79,6 +80,18 @@ The main attributes or a task are:
7980
with its logs. Set it when creating or updating a task, then filter tasks by exact match on this
8081
value in the web UI.
8182

83+
<a name="parent"></a>
84+
`parent`
85+
86+
: The ID of the task this task is part of. Tasks can only be nested one level deep, so the parent
87+
must be a task that is not itself a child. A task's parent can not be changed once it has been
88+
set. Both rules are enforced by the API, so breaking either one causes the request to fail.
89+
90+
It can be set explicitly when creating or updating a task. The Celery and Procrastinate
91+
integrations also set it automatically for tasks enqueued from within a tracked task, and the
92+
`track` decorator sets it on a decorated function called from within one. Each integration
93+
excludes some cases — see [Python SDK](python.md#parent-and-child-tasks) for details.
94+
8295
### Example Task
8396

8497
```json
@@ -106,7 +119,8 @@ The main attributes or a task are:
106119
{"environment": "production"}
107120
],
108121
"queue": "default",
109-
"external_id": "celery-abc-123"
122+
"external_id": "celery-abc-123",
123+
"parent": null
110124
}
111125
```
112126

docs/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ task.update(status=StatusEnum.SUCCESS, value=100)
3131
Or monitor Celery tasks automatically with the [Celery integration](python-celery.md):
3232

3333
```python
34-
from taskbadger.systems import CelerySystemIntegration
34+
from taskbadger.systems.celery import CelerySystemIntegration
3535

3636
taskbadger.init(
3737
token="YOUR_API_KEY",

docs/python-celery.md

Lines changed: 163 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ task that is executed by the Celery workers (except the internal Celery tasks),
1919

2020
```python
2121
import taskbadger
22-
from taskbadger.systems import CelerySystemIntegration
22+
from taskbadger.systems.celery import CelerySystemIntegration
2323

2424
taskbadger.init(
2525
token="YOUR_API_KEY",
@@ -107,9 +107,22 @@ The Task Badger task will also be updated when the task completes.
107107
### Task Customization
108108

109109
You can pass additional parameters to the Task Badger `Task` class which will be used when creating the task.
110-
This can be done by passing keyword arguments prefixed with `taskbadger_` to the `.appy_async()` function or
110+
This can be done by passing keyword arguments prefixed with `taskbadger_` to the `.apply_async()` function or
111111
to the task decorator.
112112

113+
!!! warning "`taskbadger_` arguments to `apply_async` require the task base class"
114+
115+
It is `taskbadger.celery.Task` that intercepts `taskbadger_`-prefixed arguments to `apply_async`, so
116+
those only work on tasks declared with `base=Task`. On a plain Celery task they are silently ignored:
117+
the task still publishes, and is still tracked if the
118+
[system integration](#celery-system-integration) tracks it, but the options have no effect.
119+
120+
`taskbadger_` arguments on the **task decorator** are not affected. They are read off the task class
121+
when the task is published, so they apply whether or not the task uses `base=Task`.
122+
123+
To set options per call on a task that is tracked by the system integration alone, pass them in the
124+
message headers instead — see [Customization without the task base class](#customization-without-the-task-base-class).
125+
113126
```python
114127
# using the task decorator
115128

@@ -120,14 +133,14 @@ def my_task(arg1, arg2):
120133

121134
# using individual keyword arguments
122135
my_task.apply_async(
123-
arg1, arg2,
136+
args=[arg1, arg2],
124137
taskbadger_name="my task",
125138
taskbadger_value_max=1000,
126139
taskbadger_data={"custom": "data"},
127140
)
128141

129142
# using a dictionary
130-
my_task.apply_async(arg1, arg2, taskbadger_kwargs={
143+
my_task.apply_async(args=[arg1, arg2], taskbadger_kwargs={
131144
"name": "my task",
132145
"value_max": 1000,
133146
"data": {"custom": "data"}
@@ -151,6 +164,58 @@ my_task.apply_async(arg1, arg2, taskbadger_kwargs={
151164
==Since v1.4.0==
152165

153166

167+
### Customization without the task base class
168+
169+
Task Badger options can also be passed in the Celery message headers, which works for any task,
170+
including plain Celery tasks tracked by the `CelerySystemIntegration`:
171+
172+
```python
173+
my_task.apply_async(
174+
args=[arg1, arg2],
175+
headers={"taskbadger_kwargs": {
176+
"name": "my task",
177+
"value_max": 1000,
178+
"data": {"custom": "data"},
179+
}},
180+
)
181+
```
182+
183+
Unlike the `taskbadger_`-prefixed arguments, the header is read when the task is published rather than
184+
by the task class, so no `base=Task` is needed. It takes the same options as `taskbadger_kwargs`, minus
185+
the prefix, and takes precedence over values set on the task decorator.
186+
187+
!!! warning "Eager tasks read fewer options from the header"
188+
189+
That applies to tasks which are actually published. When Celery runs a task [eagerly][always_eager]
190+
nothing is published, so the Task Badger task is created as the task starts instead, reading the
191+
header directly. Only `parent`, `heartbeat_interval` and `stale_timeout` are honoured there —
192+
`name`, `value_max` and `data` are ignored.
193+
194+
`taskbadger_track` is also *required* for an eager task that doesn't use `base=Task`, even if the
195+
system integration would otherwise track it.
196+
197+
If the task would not otherwise be tracked — it isn't using the base class and doesn't match the system
198+
integration's tracking rules — add `taskbadger_track` to the headers to track it anyway:
199+
200+
```python
201+
my_task.apply_async(
202+
args=[arg1, arg2],
203+
headers={
204+
"taskbadger_track": True,
205+
"taskbadger_kwargs": {"name": "my task"},
206+
},
207+
)
208+
```
209+
210+
!!! note
211+
212+
`record_task_args` is a header of its own rather than an entry in `taskbadger_kwargs`:
213+
`headers={"taskbadger_record_task_args": True}`.
214+
215+
The `taskbadger_task_id` attribute and `get_taskbadger_task()` method of the
216+
[result object](#basic-usage) are added by `taskbadger.celery.Task`, so they are not available on
217+
the result when using headers alone.
218+
154219
### Accessing the Task Object
155220

156221
The `taskbadger.celery.Task` class provides access to the Task Badger task object via the `taskbadger_task` property
@@ -269,6 +334,65 @@ Task Badger will create task records for each inner invocation with metadata sim
269334
}
270335
```
271336

337+
## Subtasks
338+
339+
==Since v2.5.0==
340+
341+
A Celery task published from inside a tracked task is automatically nested under it via the
342+
[`parent`](data_model.md#parent) field, so you can see the work a task spawned.
343+
344+
Tasks nest a single level deep. A task published by a task that is itself a child becomes a sibling
345+
of that child rather than a grandchild.
346+
347+
What gets nested:
348+
349+
- Tasks published from the body of a tracked task via `.delay()` or `.apply_async()`, including when
350+
Celery runs them eagerly.
351+
- Retries, which are nested under the first attempt.
352+
353+
What does not get nested:
354+
355+
- The next link of a `chain`, and `link` callbacks. These are successors of the task rather than work
356+
it chose to enqueue.
357+
- Tasks produced by [canvas primitives](#canvas-primitives-map-starmap-chunks) (`map`, `starmap`,
358+
`chunks`) when they run on a worker. Their Task Badger tasks are created in the worker rather than
359+
at publish time, so the enclosing task isn't visible there. They do nest when Celery runs eagerly.
360+
361+
!!! note
362+
363+
Known edge case: if a chain's next link is *also* called directly from the task body, that direct
364+
call is not nested.
365+
366+
To override the automatic nesting, pass `taskbadger_parent` for the call. An explicit `None` makes the
367+
task a root task even though it was published from inside a tracked task:
368+
369+
```python
370+
# nest under a different task
371+
my_task.apply_async(args=[arg1, arg2], taskbadger_parent=parent_task.id)
372+
373+
# opt out of nesting
374+
my_task.apply_async(args=[arg1, arg2], taskbadger_parent=None)
375+
```
376+
377+
`taskbadger_parent` takes a **Task Badger** task ID, not a Celery task or result ID.
378+
379+
As with the other `taskbadger_` arguments to `apply_async` this requires the task to use `base=Task`;
380+
without it, pass `headers={"taskbadger_kwargs": {"parent": None}}` instead. See
381+
[Customization without the task base class](#customization-without-the-task-base-class).
382+
383+
==Since v2.5.1== eager tasks honour `taskbadger_parent`; before that they always nested under the
384+
enclosing task.
385+
386+
!!! warning "Canvas primitives ignore `taskbadger_parent`"
387+
388+
[Canvas primitives](#canvas-primitives-map-starmap-chunks) (`map`, `starmap`, `chunks`) are
389+
published under Celery's own `celery.map` / `celery.starmap` task rather than your own, so
390+
`taskbadger_parent` is never intercepted and the `taskbadger_kwargs` header doesn't reach the
391+
worker. On a worker their parent can't be set per call, and they are not nested at all.
392+
393+
Run eagerly they do nest under the enclosing task, and there the parent can be overridden with
394+
`headers={"taskbadger_kwargs": {"parent": ...}}`.
395+
272396
## External ID
273397

274398
The Celery task ID is automatically recorded on the Task Badger task's
@@ -277,8 +401,42 @@ originating Celery task.
277401

278402
## Opting out
279403

280-
If you want to prevent TaskBadger from tracking a particular execution, set the `taskbadger_track` header (False) when publishing:
404+
To stop Task Badger tracking a single execution, set `taskbadger_track` to `False`, either as an
405+
argument to `apply_async` or in the message headers:
281406

282407
```python
408+
# as an argument — needs base=Task
409+
my_task.apply_async(args=[arg1, arg2], taskbadger_track=False)
410+
411+
# in the headers — works for any task
412+
my_task.apply_async(args=[arg1, arg2], headers={"taskbadger_track": False})
413+
414+
# canvas primitives take the header form only
283415
add.map([(1, 2), (2, 3)]).apply_async(headers={"taskbadger_track": False})
284416
```
417+
418+
This takes precedence over the `CelerySystemIntegration`, so it opts out even when `auto_track_tasks`
419+
is on, and over the `base=Task` class.
420+
421+
The value must be exactly `False`. Omitting it leaves tracking to the normal rules.
422+
423+
!!! warning "The argument form needs the task base class"
424+
425+
As with the other [`taskbadger_` arguments to `apply_async`](#task-customization),
426+
`taskbadger_track=False` is intercepted by `taskbadger.celery.Task`, so it only works on tasks
427+
declared with `base=Task`. On a plain Celery task, or on a
428+
[canvas primitive](#canvas-primitives-map-starmap-chunks), it is silently ignored and the task
429+
stays tracked.
430+
431+
The header form works in all three cases, so prefer it unless you know the task uses `base=Task`.
432+
433+
To exclude a task from tracking on every call rather than per execution, use the `excludes` argument to
434+
[`CelerySystemIntegration`](#celery-system-integration), which matches on task name.
435+
436+
!!! warning "Only works from v2.5.2"
437+
438+
==Since v2.5.2==
439+
440+
Before that this header could only ever *enable* tracking, never suppress it. `False` was
441+
indistinguishable from omitting the header, and `apply_async` on a `base=Task` task overwrote it
442+
with `True`. On earlier versions use `excludes` instead.

docs/python-decorator.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,27 @@ status to `success` when the function completes or `error` if an exception is ra
1717
The decorator also applies the `taskbadger.Session` context manager to the function.
1818
See [connection management](python.md#connection-management).
1919

20+
When the function raises, the exception is recorded on the task data along with any context from the
21+
configured [context providers](python.md#error-context-providers).
22+
23+
## Nested tasks
24+
25+
==Since v2.5.0==
26+
27+
Tasks tracked by another integration while a decorated function is running are nested under its task
28+
via the [`parent`](data_model.md#parent) field. That means other `@track` decorated functions called
29+
from the body, as well as [Celery](python-celery.md#subtasks) and
30+
[Procrastinate](python-procrastinate.md#subtasks) tasks enqueued from it. A bare `Task.create` in the
31+
function body is not nested unless you pass `parent` yourself.
32+
33+
Each integration carves out some exceptions — Celery, for instance, doesn't nest chain successors,
34+
`link` callbacks, or canvas primitives running on a worker. The [Celery](python-celery.md#subtasks) and
35+
[Procrastinate](python-procrastinate.md#subtasks) pages list what does and doesn't get nested.
36+
37+
Tasks nest a single level deep, so anything created by a decorated function that is itself a child
38+
becomes a sibling of that child rather than a grandchild. Passing `parent` to the decorator explicitly
39+
overrides the automatic nesting.
40+
2041
## API Docs
2142

22-
::: taskbadger.track
43+
::: taskbadger.track

docs/python-procrastinate.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,19 @@ Task Badger task, so you can monitor the history and health of your scheduled jo
206206
The name of the Procrastinate queue a task is deferred to is automatically recorded on the Task Badger
207207
task's [`queue`](data_model.md#queue) field.
208208

209+
## Subtasks
210+
211+
==Since v2.5.0==
212+
213+
A job deferred from inside a tracked task is automatically nested under it via the
214+
[`parent`](data_model.md#parent) field, so you can see the work a job spawned.
215+
216+
Tasks nest a single level deep, so a job deferred by a task that is itself a child becomes a sibling
217+
of that child rather than a grandchild.
218+
219+
Nesting relies on the same wrapping as the rest of the integration, so the cases listed under
220+
[Known Limitations](#known-limitations) are neither tracked nor nested.
221+
209222
## External ID
210223

211224
The Procrastinate job ID is automatically recorded on the Task Badger task's

0 commit comments

Comments
 (0)