---
title: "Reorder Tasks"
method: POST
path: "/tasks/reorder"
tags: ["Tasks"]
---

# Reorder Tasks

`POST /tasks/reorder`

## Request

- Base URL: `https://api.purplegrain.com/api/v1`
- URL: `https://api.purplegrain.com/api/v1/tasks/reorder`
- Auth: HTTP bearer

One write per drag. Send the ids of one sibling set — a parent's
sub-tasks, or an owner's top-level tasks — in their new order, and the
server rewrites `position` as 1…N inside a single transaction. Nothing
else moves: status and completed_at are untouched. Every task must be
editable by the caller (its grower or assigned contractor); all must sit
under the given `parent` (or all be top-level tasks of one owner when no
parent is given), every id must resolve and none may repeat — otherwise
nothing is written. Responds with the reordered tasks in board order.
Send the whole sibling set: a partial list only rewrites the rows named,
and their positions can then collide with siblings left out.

## Query parameters

- `include` string

## Request body

- TaskReorderModelAttributes: Flat attributes-adapter request body for POST /tasks/reorder (Content-Type application/json). `data.ids` is the sibling set in its new order; `data.parent` is a `{ id, type }` resource identifier for the parent those tasks sit under, or null (or omitted) for an owner's top-level tasks. Equivalent JSON:API (nested) shape: `TaskReorderModel` sent as application/vnd.api+json.
  - `data` object, required
    - `ids` string[], required: Task ids in their new order. The first becomes position 1.
    - `parent` object, nullable: The parent whose sub-tasks are being reordered; null or omitted for an owner's top-level tasks.
      - `id` string
      - `type` 'task'

## Response `200`

The reordered tasks, board ordered. When the `include` query parameter is used, the response may contain an `included` array (JSON:API) or inlined related resources (application/json).

- TasksIndexResponseAttributes
  - `data` TaskModelAttributes[]
    - `id` string
    - `type` string
    - `status` 'pending' | 'cancelled' | 'completed': `pending`, `cancelled` or `completed`. A treatment after the fact is `completed` with `created_as: record`. A planned treatment is `pending` with `created_as: task`.
    - `completed_at` string, date-time, nullable: When the task was completed. The server stamps it as status becomes completed and clears it when the task leaves completed, unless the client sent an explicit value — that wins, including a back-dated time. Stored and returned in UTC: `2025-12-23T09:00:00+11:00` and `2025-12-22T22:00:00Z` are the same moment. AEDT is +11:00 (first Sunday of October to first Sunday of April); AEST is +10:00. `createTreatmentRecord` stores midday in the application time zone from a `YYYY-MM-DD` date.
    - `created_at` string, date-time
    - `updated_at` string, date-time
    - `description` string, nullable
    - `tags` string[]: Hashtag-style labels (they work like Twitter hashtags) — free-form lowercase tokens stored without a leading hash, e.g. ["slashing", "north-paddock"]. Reusing a tag groups records together, and the /tasks/search and /services/search term parameter matches tags case-insensitively, so a tag aggregates everything labelled with it. A drench may carry `drench`, one lowercase-hyphenated tag per product, `whp-until-YYYY-MM-DD`, `esi-until-YYYY-MM-DD`, `needs-review` when something is unconfirmed, and `import-<source-id>` to group one imported sheet. Withholding dates come from the product days, not from tags. Always an array — the API never returns null.
    - `image_url` string, nullable
    - `image_thumb_url` string, nullable
    - `image_ids` string[], nullable: Attachment ids for the photo gallery, aligned index-for-index with image_urls. Use one as the image_id in DELETE /tasks/{id}/images/{image_id}.
    - `image_urls` string[], nullable: The photo gallery, oldest first — the first entry is the cover (also exposed as the singular image_url).
    - `image_thumb_urls` string[], nullable: Thumbnail variants of image_urls, same order.
    - `images` TaskImage[], nullable: The gallery, oldest first. Position 0 is the cover (`image_url`). Same order as `image_urls` and `image_ids`.
      - `id` string, required: Gallery attachment id. The same value is in `image_ids`, and it is what `remove_image_ids` and `DELETE /tasks/{id}/images/{image_id}` take.
      - `url` string, nullable
      - `thumb_url` string, nullable
      - `content_type` string, nullable
      - `byte_size` integer, nullable
      - `width` integer, nullable
      - `height` integer, nullable
      - `position` integer, required: Gallery order, starting at 0. The photo at position 0 is the cover (`image_url`).
      - `source` 'upload' | 'url' | 'base64', nullable: How the photo was attached. Blank on photos stored before this field existed, and on a multipart upload.
    - `url` string, nullable
    - `service_id` string, nullable
    - `share_scope` 'private' | 'broadcast', nullable: Who can see the task — `private` (the owner's board only) or `broadcast` (shared with local contractors). Write it through `broadcast`. An API or MCP client that sends `broadcast` or `share_scope` is refused with 403 and nothing is written.
    - `broadcast` boolean: Whether the task is shared with local contractors (`share_scope` is `broadcast`). Writable on create and update — send `true` to share, `false` to un-share; sharing is free.
    - `broadcast_payment_status` string, nullable: Legacy, read-only — sharing is free and no current operation writes this field.
    - `broadcast_paid_at` string, date-time, nullable: Legacy, read-only — see broadcast_payment_status.
    - `fulfillment_status` string, nullable
    - `position` integer, nullable: Order within the sibling set (or within the top level). Only meaningful relative to siblings; reordering rewrites it and must never touch status or completed_at.
    - `sequenced` boolean: On a parent, its children run in order. The UI numbers them and marks later steps "after step N"; completing out of sequence stays allowed, so this drives presentation only.
    - `progress` object, nullable: Derived roll-up over the children — never stored. Null when the task has no children, so a plain task is distinguishable from a campaign with nothing done yet.
      - `done` integer
      - `total` integer
    - `operation_type` string, nullable: What kind of work this is — one of `ai`, `spray`, `treatment`, `husbandry`, `fertiliser`, `drench`, `spread`, `sow`, `fieldwork`, `harvest`, `move_stock`, `shear`, `crutch`, `feed`, `fence`, `slash`, `observation`. `ai` is a PurpleGrain AI record: the farmer's own words (and photos) queued for the formatter, with no typed child and possibly no asset (see `ai_format_done_at`). Create one with `POST /tasks` and `operation_type: ai` from the PurpleGrain app or an API key (an MCP client is refused with 403: an assistant knows the type and writes the typed record itself); when the formatter finishes it replaces `ai` with the real type and writes the typed child, or clears the type and leaves a plain task if it gives up. `treatment` is an animal treatment on one mob. Save it with `createTreatmentRecord` and the appropriate `category` (`drench` for a drench, `vaccine` for a vaccination), which stores the products and derives the withholding dates. `drench` remains a deprecated operation type. `spray` is a treatment of one paddock (herbicide, pesticide, fungicide): save it with `createSprayRecord`, which stores the products and derives the graze and harvest dates. Deliberately a free string with no enum: the vocabulary is still moving, and a schema stricter than the model rejects valid traffic the day request validation is enforced — before authentication runs.
    - `capture_source` string, nullable: How the entry was captured — `typed`, `voice`, `agent` or `import`. `form` is accepted and stored as `typed`. A PurpleGrain AI record sends `voice` or `typed`. Agents send `agent`. A photographed or uploaded sheet is `import`. `createTreatmentRecord` sets `agent` itself. Free string with no enum, for the same reason as operation_type.
    - `operator` string, nullable: Who treated or did the work, as a display name or initials (for example "AH"). Free text: a name on the diary entry, not a link to a user. `createTreatmentRecord` sets this from the signed-in user and does not take it in the body; send it here when someone else treated.
    - `record_summary` string, nullable: The composed line a completed row shows on the timeline, for example "Drenched Rams (11 hd): Startect 20 mL/hd. WHP until 6 Jan 2026, ESI until 20 Jan 2026". Distinct from `description`, which stays the authored task text. `createTreatmentRecord` leaves this empty; the withholding dates are `treatment.safe_for_slaughter_on` and `treatment.export_eligible_on`.
    - `record_notes` string, nullable: The farmer's own words from the capture, unedited, rendered under the summary when the two differ. Product, batch, dose, WHP and ESI belong on `treatment.products` — that is what the withholding dates are calculated from. Expiry and route are fields of the treatment (`treatment.route`, each product's `expiry`); leave them unset rather than inventing either. Never invent a batch, expiry, WHP or ESI.
    - `drench` DrenchRead: The drench of a task or record. `safe_for_slaughter_on` and `export_eligible_on` are DERIVED (never stored) from the completion date plus the longest WHP / ESI among named products, and null while the task is open or until a named product carries a withholding period.
      - `id` string: The DrenchRecord id — the same before and after the task is completed.
      - `head` integer
      - `products` DrenchProductRead[]
        - `id` string
        - `name` string, nullable: What went in the gun. Blank is valid — a drench recorded before anyone said what it was.
        - `active` string, nullable: Label active constituent, e.g. `Abamectin 1 g/L`.
        - `batch` string, nullable: Batch number from the pack. Null when it was not recorded. Never invented.
        - `expiry` string, date, nullable: Expiry date on the pack, `YYYY-MM-DD`. Null when it was not recorded. Never invented.
        - `dose` string, nullable: As entered, e.g. `5 mL / 20 kg`.
        - `dose_value` number, nullable: The number in the dose, e.g. 5. Only what the caller sent; never parsed from `dose`.
        - `dose_unit` 'mL' | 'L' | 'g' | 'kg', nullable: Unit of `dose_value`.
        - `dose_basis` 'per_head' | 'per_kg' | 'per_mob', nullable: What `dose_value` is per.
        - `whp_days` integer: Withholding period, days.
        - `esi_days` integer: Export slaughter interval, days.
        - `position` integer: Order the products were entered in.
      - `safe_for_slaughter_on` string, date, nullable
      - `export_eligible_on` string, date, nullable
    - `spray` SprayRead: The spray of a task or record. `safe_to_graze_on` and `safe_to_harvest_on` are DERIVED (never stored) from the completion date plus the longest grazing / harvest WHP among named products, and null while the task is open or until a named product carries a value above zero for that period.
      - `id` string: The SprayRecord id — the same before and after the task is completed.
      - `area_ha` number, nullable: Hectares sprayed. Defaults to the paddock's area when the child is created without one and again when the task closes; null when the paddock has no area either.
      - `products` SprayProductRead[]
        - `id` string
        - `name` string, nullable: What went in the tank. Blank is valid — a spray recorded before anyone said what it was.
        - `active` string, nullable: Label active constituent, e.g. `Clopyralid 300 g/L`.
        - `batch` string, nullable: Batch number from the pack. Null when it was not recorded. Never invented.
        - `rate` string, nullable: As entered, e.g. `300 mL / ha`.
        - `grazing_whp_days` integer: Grazing withholding period, days.
        - `harvest_whp_days` integer: Harvest withholding period, days.
        - `position` integer: Order the products were entered in.
        - `role` 'herbicide' | 'fungicide' | 'insecticide' | 'adjuvant', nullable: What the product is in the tank.
        - `expiry` string, date, nullable: Expiry date on the pack, `YYYY-MM-DD`. Never invent one; unset prints `not recorded`.
        - `rate_value` number, nullable
        - `rate_unit` 'g' | 'kg' | 't' | 'mL' | 'L' | 'seeds' | '%', nullable: Unit of `rate_value`. A rate with a number and no unit is refused (422).
        - `rate_basis` 'ha' | 'head' | '100_kg_seed' | '100_l' | 'total', nullable: What the rate is per.
        - `esi_days` integer
        - `egi_days` integer
        - `eafi_days` integer
        - `do_not_graze` boolean: The label says DO NOT graze. Recorded on the spray and shown on the record; it gives the paddock no status, since it has no date.
        - `product_id` string, nullable: Optional link to the catalogue; the values here are copied at save.
      - `safe_to_graze_on` string, date, nullable
      - `safe_to_harvest_on` string, date, nullable
      - `method` 'boom' | 'boomless' | 'aerial' | 'drone' | 'spot' | 'weed_wiper' | 'knapsack' | 'airblast', nullable: How it was applied.
      - `water_rate_l_ha` number, nullable
      - `nozzle` string, nullable: Brand, model and size, as entered.
      - `pressure_kpa` integer, nullable: Boom pressure, kPa.
      - `boom_height_m` number, nullable
      - `speed_kmh` number, nullable
      - `droplet_category` 'extremely_fine' | 'very_fine' | 'fine' | 'medium' | 'coarse' | 'very_coarse' | 'extremely_coarse' | 'ultra_coarse', nullable: Droplet size category.
      - `no_spray_zone_m` integer, nullable: Downwind no-spray zone kept, metres.
      - `re_entry_note` string, nullable: Re-entry period, worker safety, informational.
      - `neighbour_drift` boolean, nullable: Drift from a neighbour was a factor.
      - `do_not_graze` boolean: A named product's label says DO NOT graze. Recorded on the spray; it gives the paddock no status, since it has no date.
      - `weather_readings` SprayWeatherReadingRead[]
        - `id` string
        - `read_at` string, date-time, nullable: When it was taken, an ISO 8601 time.
        - `wind_kmh` union: Wind speed, km/h.
          - number
          - string
        - `gust_kmh` union: Gust, km/h.
          - number
          - string
        - `wind_dir` string, nullable: Compass point, e.g. `SW`.
        - `temp_c` number, nullable: Temperature, degrees C.
        - `rh_pct` integer, nullable: Relative humidity, per cent.
        - `delta_t` number, nullable: Delta T.
        - `cloud_pct` integer, nullable: Cloud cover, per cent.
        - `rain_mm` union: Rain, mm.
          - number
          - string
        - `inversion_observed` boolean, nullable: An inversion was observed.
        - `source` string, nullable: On-site, or a named station with its distance.
        - `position` integer
    - `treatment` TreatmentRead: The treatment of a task or record. Null on a task that has none.
      - `id` string: The TreatmentRecord id: the same before and after the task is completed.
      - `safe_for_slaughter_on` string, date, nullable: DERIVED, never stored: the completion date plus the longest meat WHP among named products. Null while the task is open or until a named product carries one.
      - `export_eligible_on` string, date, nullable: DERIVED: the completion date plus the longest ESI among named products.
      - `category` 'drench' | 'fluke' | 'lice' | 'flystrike' | 'other_external_parasite' | 'vaccine' | 'vitamin_mineral' | 'antibiotic' | 'pain_relief' | 'feed_water_additive' | 'foot_treatment' | 'reproductive' | 'hgp' | 'other', nullable: What kind of treatment. Required on create. A `drench` is the everyday word for category `drench`.
      - `route` 'oral' | 'injection' | 'backline' | 'jetting' | 'dip' | 'capsule' | 'topical' | 'in_feed_water', nullable: How it was given. Leave it unset if the farmer did not say; it prints `not recorded`.
      - `head` integer, nullable: Head treated; separators are stripped ("1,240" is 1240). Defaults to the mob's head count.
      - `equipment_cleaned` boolean, nullable: ISC checklist item; unset is not recorded.
      - `adverse_reaction` string, nullable: Any adverse reaction or broken needle. LPA asks for both to be recorded and passed on.
      - `hgp_earmarked` boolean, nullable: HGP implant: triangle ear mark applied (cattle).
      - `vet_authorisation` string, nullable: Off-label use: who authorised it.
      - `products` TreatmentProductRead[]
        - `id` string
        - `name` string, nullable: Product trade name. Blank is valid, but a blank name contributes nothing to the derived withholding dates.
        - `active` string, nullable: Label active constituent, e.g. `Moxidectin 1 g/L`.
        - `batch` string, nullable: Batch number from the pack. Never invent one; leave it unset until the farmer confirms it.
        - `expiry` string, date, nullable: Expiry date on the pack, `YYYY-MM-DD`. Never invent one; unset prints `not recorded`.
        - `dose` string, nullable: The dose as entered, e.g. `5 mL / 20 kg`. Kept verbatim.
        - `dose_value` number, nullable: The number in the dose, e.g. 5. Only what the caller sends; it is never parsed from `dose`.
        - `dose_unit` 'mL' | 'L' | 'g' | 'kg', nullable: Unit of `dose_value`.
        - `dose_basis` 'per_head' | 'per_kg' | 'per_mob', nullable: What `dose_value` is per.
        - `drench_group` string, nullable: Chemical group, e.g. `BZ`, `LV`, `ML`, `AD`, `SI`, `OP`, `TZ`, `SA`, `IQ` or a combination, for resistance tracking.
        - `whp_days` integer, nullable: Meat withholding period from the label, in whole days. "14 d" is read as 14. Never invent it.
        - `esi_days` integer, nullable: Export slaughter interval from the label, in whole days. If the label gives none, send the WHP. Never invent it.
        - `wool_interval_days` integer, nullable: Wool harvest interval, days. 0 is none stated.
        - `milk_whp_days` integer, nullable: Milk withholding period, days. 0 is none stated.
        - `product_id` string, nullable: Optional link to the catalogue (`listProducts`). The values here are copied at save, so a later catalogue edit never changes a past record.
        - `position` integer: Order the products were entered in.
    - `husbandry` HusbandryRead: The husbandry of a task or record. Null on a task that has none.
      - `id` string: The HusbandryRecord id.
      - `marking_percent` integer, nullable: Lambs marked as a share of ewes joined, worked out when read. Null until both are said.
      - `safe_for_slaughter_on` string, date, nullable: DERIVED from the products' meat WHP; null when no named product carries one.
      - `export_eligible_on` string, date, nullable: DERIVED from the products' ESI.
      - `head` integer, nullable: Head the work was done to; separators are stripped. Defaults to the mob's head count.
      - `procedures` string[]: What was done. One or more.
      - `method` string, nullable: Free text, e.g. rings or knife.
      - `outcomes` object: One block per procedure, e.g. `{ "mark": { "lambs_marked": 412, "ewes_joined": 350 } }`. Figures are numbers, zero or more, except `shearer`, which is text.
      - `products` HusbandryProductRead[]
        - `id` string
        - `name` string, nullable: Product trade name. Blank is valid, but a blank name contributes nothing to the derived withholding dates.
        - `active` string, nullable: Label active constituent, e.g. `Moxidectin 1 g/L`.
        - `batch` string, nullable: Batch number from the pack. Never invent one; leave it unset until the farmer confirms it.
        - `expiry` string, date, nullable: Expiry date on the pack, `YYYY-MM-DD`. Never invent one; unset prints `not recorded`.
        - `dose` string, nullable: The dose as entered, e.g. `5 mL / 20 kg`. Kept verbatim.
        - `dose_value` number, nullable: The number in the dose, e.g. 5. Only what the caller sends; it is never parsed from `dose`.
        - `dose_unit` 'mL' | 'L' | 'g' | 'kg', nullable: Unit of `dose_value`.
        - `dose_basis` 'per_head' | 'per_kg' | 'per_mob', nullable: What `dose_value` is per.
        - `drench_group` string, nullable: Chemical group, e.g. `BZ`, `LV`, `ML`, `AD`, `SI`, `OP`, `TZ`, `SA`, `IQ` or a combination, for resistance tracking.
        - `whp_days` integer, nullable: Meat withholding period from the label, in whole days. "14 d" is read as 14. Never invent it.
        - `esi_days` integer, nullable: Export slaughter interval from the label, in whole days. If the label gives none, send the WHP. Never invent it.
        - `wool_interval_days` integer, nullable: Wool harvest interval, days. 0 is none stated.
        - `milk_whp_days` integer, nullable: Milk withholding period, days. 0 is none stated.
        - `product_id` string, nullable: Optional link to the catalogue (`listProducts`). The values here are copied at save, so a later catalogue edit never changes a past record.
        - `position` integer: Order the products were entered in.
    - `fertiliser` FertiliserRead: The fertiliser of a task or record. Null on a task that has none.
      - `id` string: The FertiliserRecord id.
      - `nutrients_applied` object: Kilograms per hectare of each nutrient across the lines that state an analysis and a rate per hectare in kg or t. Worked out when read.
      - `area_ha` number, nullable: Hectares covered; separators are stripped. Defaults to the paddock's area.
      - `method` 'broadcast' | 'banded' | 'drilled' | 'in_furrow' | 'fertigation' | 'foliar', nullable: How it was applied.
      - `incorporation` 'none' | 'mechanical' | 'rain' | 'irrigation', nullable: How it was incorporated.
      - `incorporated_at` string, date-time, nullable: When it was incorporated, an ISO 8601 time.
      - `spreader` string, nullable: The spreader used.
      - `bout_width_m` number, nullable: Spread width, metres.
      - `cost_per_ha` number, nullable: Cost per hectare, for gross margins.
      - `products` FertiliserProductRead[]
        - `id` string
        - `name` string, nullable: Product, e.g. `Urea`. Blank is valid.
        - `analysis` object: Nutrient percentages, e.g. `{ "N": 46 }`, copied from the catalogue so a later edit never changes a past record.
        - `rate` string, nullable: The rate as entered, e.g. `100 kg/ha`. Kept verbatim.
        - `rate_value` number, nullable: The number in the rate. Parsed from `rate` when omitted.
        - `rate_unit` 'g' | 'kg' | 't' | 'mL' | 'L', nullable: Unit of `rate_value`. Required when `rate_value` is set.
        - `rate_basis` 'ha' | 'total', nullable: What the rate is per.
        - `batch` string, nullable: Batch number. Never invent one.
        - `supplier` string, nullable: Who supplied it.
        - `advisory_note` string, nullable: Grey advice, never a status: e.g. wait three to four weeks before grazing.
        - `product_id` string, nullable: Optional link to the catalogue.
        - `position` integer: Order the products were entered in.
        - `total_kg` number, nullable: The whole quantity put on, in kilograms: the rate per hectare times the area covered. Null when either is missing.
    - `sowing` SowingRead: The sowing of a task or record. Null on a task that has none.
      - `id` string: The SowingRecord id.
      - `crop_sown` string, nullable: What the paddock carries once this is completed: the seed line's variety, else `crop`.
      - `safe_to_graze_on` string, date, nullable: DERIVED: the completion date plus the longest grazing WHP among named seed-treatment lines. Null while the task is open or until one carries it.
      - `area_ha` number, nullable: Hectares sown; separators are stripped. Defaults to the paddock's area.
      - `crop` string, nullable: e.g. `Barley`. The variety goes on the seed line.
      - `row_spacing_mm` integer, nullable: Row spacing, millimetres.
      - `depth_mm` integer, nullable: Sowing depth, millimetres.
      - `seeder` string, nullable: The seeder used.
      - `opener` string, nullable: Opener type.
      - `speed_kmh` number, nullable: Sowing speed, km/h.
      - `stubble` string, nullable: Stubble at sowing.
      - `soil_moisture` string, nullable: Soil moisture at sowing.
      - `target_plants_m2` number, nullable: Target plant density, plants per square metre.
      - `products` SowingProductRead[]
        - `id` string
        - `role` 'seed' | 'fertiliser' | 'seed_treatment' | 'inoculant', nullable: What this line is.
        - `name` string, nullable: Product or seed, e.g. `Spartacus CL barley`. Blank is valid.
        - `variety` string, nullable: Seed lines only. Completing the sowing sets the paddock's current crop to this.
        - `seed_source` 'retained' | 'purchased' | 'shared', nullable: Seed lines only.
        - `seed_lot` string, nullable: Seed lot or certificate number.
        - `germination_pct` number, nullable: Germination, 0 to 100.
        - `tgw_g` number, nullable: Thousand-grain weight, grams.
        - `rate` string, nullable: The rate as entered, e.g. `80 kg/ha`. Kept verbatim.
        - `rate_value` number, nullable: The number in the rate. Parsed from `rate` when omitted.
        - `rate_unit` 'g' | 'kg' | 't' | 'mL' | 'L' | 'seeds' | '%', nullable: Unit of `rate_value`. Required when `rate_value` is set.
        - `rate_basis` 'ha' | 'head' | '100_kg_seed' | '100_l' | 'total', nullable: What the rate is per.
        - `placement` 'with_seed' | 'banded', nullable: Fertiliser lines only.
        - `active` string, nullable: Seed treatments: the active constituent.
        - `analysis` object: Fertiliser lines: nutrient percentages by weight, lower-case keys, e.g. `{ "n": 10, "p": 22 }`, copied from the catalogue.
        - `batch` string, nullable: Batch number. Never invent one.
        - `expiry` string, date, nullable: Expiry date, `YYYY-MM-DD`.
        - `grazing_whp_days` integer, nullable: Seed treatments only: the grazing withholding period from the label, in whole days. Never invent it.
        - `esi_days` integer, nullable: Seed treatments only: the export slaughter interval, in whole days.
        - `inoculant_group` string, nullable: Inoculants only: the group, e.g. `E` for lentils.
        - `product_id` string, nullable: Optional link to the catalogue.
        - `position` integer: Order the products were entered in.
    - `fieldwork` FieldworkRead: The field work of a task or record. Null on a task that has none.
      - `id` string: The FieldworkRecord id.
      - `kind` 'cultivate' | 'harvest' | 'hay_silage' | 'slash' | 'roll' | 'burn' | 'irrigate', nullable: What was done.
      - `area_ha` number, nullable: Hectares worked; separators are stripped. Defaults to the paddock's area.
      - `implement` string, nullable: The implement or machine, e.g. `Case 8250`.
      - `outcomes` object: One block per kind, e.g. `{ "harvest": { "tonnes": 399, "yield_t_ha": 4.2, "grade": "Malt 1" } }`.
      - `products` FieldworkProductRead[]
        - `id` string
        - `name` string, nullable: Optional: a hay preservative. Blank is valid.
        - `active` string, nullable: Active constituent.
        - `batch` string, nullable: Batch number. Never invent one.
        - `expiry` string, date, nullable: Expiry date, `YYYY-MM-DD`.
        - `rate` string, nullable: The rate as entered. Kept verbatim.
        - `rate_value` number, nullable: The number in the rate. Parsed from `rate` when omitted.
        - `rate_unit` 'g' | 'kg' | 't' | 'mL' | 'L' | 'seeds' | '%', nullable: Unit of `rate_value`.
        - `rate_basis` 'ha' | 'head' | '100_kg_seed' | '100_l' | 'total', nullable: What the rate is per.
        - `product_id` string, nullable: Optional link to the catalogue.
        - `position` integer: Order the products were entered in.
    - `warnings` object[], nullable: Field work only: what the paddock was held back from on the day of the work (a harvest before a harvest withholding period ends, hay or silage before a grazing one does). Null for any other task, an empty list when there is nothing to say. A warning never blocks the write.
      - `code` 'harvest_before_withholding_ends' | 'hay_before_grazing_withholding_ends'
      - `message` string
      - `ends_on` string, date
      - `products` string[]
    - `created_as` 'task' | 'record', nullable: `task` when the row was created open and completed later, `record` when it was captured already done. A treatment after the fact is `status: completed` and `created_as: record`. A planned treatment is `status: pending` and `created_as: task`, and is completed later. `createTreatmentRecord` sets both from the date: a future date is the open task. Set once at creation and ignored on update. `can_reopen` is true only when this is `task`.
    - `can_reopen` boolean: Read-only. Whether the Reopen affordance applies — true exactly when `created_as` is `task`. A record that was born done has no open task to reopen to.
    - `ai_format_done_at` string, date-time, nullable: Read-only. When PurpleGrain AI finished with this task. Null while it is queued (and for every task that never was an AI record). Set once: a task with this set is never formatted again, and editing it does not re-queue it.
    - `ai_written_at` string, date-time, nullable: Read-only. When PurpleGrain AI first wrote this task's typed details. Null until it has, and for every task it never formatted. Set once: a re-run rewrites the details but does not move it. The app shows "Details written by PurpleGrain AI 3 Sep" and offers Re-run from it.
    - `ai_queued` boolean: Read-only. Whether PurpleGrain AI holds this task right now: a record saved as `ai` that has no typed child yet, or any task sent (back) with `rerun_ai` and not finished. Read this, not `operation_type`, for the queued state: after a re-run the type and the typed details stay while the task waits.
    - `ai_format_meta` object: Read-only. PurpleGrain AI's audit handle for this task, `{}` until the formatter has run: `outcome` (`clean`, `ambiguous`, `unplaced`, `needs_input`, `not_a_record`, `skipped`, `retrying` or `gave_up`), `model`, `input_tokens`, `output_tokens`, `latency_ms`, `prompt_hash`, `questions` (what the farmer must supply), `candidates` (the mobs or paddocks the words could mean, `{kind, id, name}`, when the asset was left unassigned), and `unconfirmed` (the product fields, as `products.0.batch`, that the farmer's words and photos do not support: stored, never confirmed). The model's raw response is not served.
    - `from_paddock_id` string, nullable: Where a mob was before this record moved it, when it was anywhere. Null on a first placement.
    - `to_paddock_id` string, nullable: Where a mob went. Set by POST /mobs/{id}/move, and by completing a task about a mob whose text carries a `#Paddock` hashtag. A mob's current paddock is derived from this column on its most recent completed record — never from `operation_type`.
    - `asset` object, nullable: Inlined when include=asset — the paddock or mob this work is about, as a PaddockModelAttributes or MobModelAttributes
    - `producer` object: Inlined when include=producer
    - `provider` object: Inlined when include=provider
    - `place` object: Inlined when include=place
    - `service` object: Inlined when include=service
    - `category` object: Inlined when include=category
    - `farm` object, nullable: Inlined when include=farm
    - `parent` object, nullable: Inlined when include=parent
    - `children` object[]: Inlined when include=children — the sub-tasks in board order, each a TaskModelAttributes
  - `meta` PaginationMeta
    - `current_page` number
    - `next_page` number, nullable
    - `prev_page` number, nullable

## Other responses

- `401`: Unauthenticated
- `403`: The caller may not edit at least one of the tasks
- `422`: Not one sibling set — an empty or repeated list, an unknown id, a task under a different parent, or top-level tasks from more than one owner

---

[API](https://skmtc.dev/purplegrain/apis/purple-grain-api.md) · [All operations](https://skmtc.dev/purplegrain/apis/purple-grain-api/llms.txt) · [OpenAPI document](https://skmtc.dev/purplegrain/apis/purple-grain-api/revisions/a9136f78e9fb?raw)
