---
title: "Generate Time Entries"
method: POST
path: "/v1/time-entries/generate"
tags: ["time-entries"]
---

# Generate Time Entries

`POST /v1/time-entries/generate`

## Request

- Base URL: `https://app.tokumalabs.com/api`
- URL: `https://app.tokumalabs.com/api/v1/time-entries/generate`
- Auth: OAuth 2
  - Password
  - Token URL: `v1/auth/token`

Rebuild a day's timesheet, or decide it does not need rebuilding.

Answers one of three ways, and **none of them is an error**: a new run, the
run already in flight (adopt it and watch its progress), or nothing —
because the day was rebuilt moments ago or nothing has changed since. The
third is a 200 carrying ``outcome=skipped`` and no ``task_id``.

That shape is what lets the client's Refresh control run on every press and
every window focus without asking permission. Refreshing the *data* is
free and always happens client-side; reprocessing the *day* costs a
reasoning call, so the server declines it silently rather than telling the
user their gesture was rejected. Whichever way it answers, the worker
writes status onto the ``generation_tasks`` row and the desktop client
observes it via the cursor sync feed (``GET /api/v1/sync``).

A request carrying no ``trigger`` comes from a client predating this
behaviour and takes the old path exactly: ``mode`` is honoured, a run is
always minted, and a collision is a 409.

## Request body

- GenerateTimeEntriesRequest: Request model for on-demand time entry generation.
  - `date` string, date, required: Date to generate entries for
  - `trigger` 'refresh' | 'focus' | 'rebuild': What the user did, in the client's vocabulary. Deliberately not the same word list as ``GenerationProducer``: this is the *public* surface, so it names gestures ("the user pressed Refresh") rather than server-side producers, and the server maps it. The mapping is where the app's focus trigger becomes distinguishable from a press without the client having to know what a producer is.
  - `mode` 'full' | 'after'

## Response `202`

Successful Response

- GenerationTaskResponse: Response for generation task creation and status polling. Failure information uses the same (code, message) shape as the synchronous APIException envelope, but persisted on the row and surfaced asynchronously. The client branches on the *_code field for localized rendering and falls back to the *_message text for codes it doesn't yet know. See server/AGENTS.md → "Persisted async errors" for the contract.
  - `outcome` 'started' | 'adopted' | 'skipped': What the server did about this request. ``skipped`` is a success, not a refusal — see ``GenerationTaskResponse.outcome``.
  - `skip_reason` 'spacing_floor' | 'day_closed' | 'failure_capped' | 'no_blocks' | 'nothing_new': Why the server declined to reprocess the day. Present only on ``outcome=skipped``, and it exists because the client had no way to tell these apart: every one of them rendered as "Already up to date", including the two that are not remotely that. A day whose rebuild was failing told the user their timesheet was current, for six hours at a time. Mirrors the SKIP reasons of :func:`shared.time_entry_processing.refresh_gate.evaluate_refresh`. That gate types its reasons as plain strings (one field carries both trigger arms and skip reasons), so the mapping is by value in ``_skip_reason`` — which returns ``None`` for anything it does not recognise, so a reason added to the gate degrades to "no reason given" rather than a 500 on a response that was otherwise fine. ``tests/unit/.../test_router_smoke.py`` pins the five. The two arms a client should treat differently from the rest: ``failure_capped`` is a broken day, and ``day_closed`` is one only "Rebuild from scratch" can reopen. Neither is "up to date". **``failure_capped`` no longer reaches a Refresh press**, because the gate stopped applying the cap to one — a press now runs, and if the run fails the user started it and gets the failure card. It is still in the vocabulary and still worth rendering: the focus trigger keeps the cap, a previous-release server mid-rollout still sends it, and the value is the gate's to define rather than this endpoint's to prune. Do not read its presence here as evidence a press can still be capped.
  - `task_id` string, nullable: Unique task identifier. Null only when `outcome` is `skipped` — there is no run to watch.
  - `date` string, date, required: The date this generation targets
  - `status` string, nullable: Current task status. Null when `outcome` is `skipped`. A string rather than a closed enum: the adoption path returns an existing row, which may carry a status a newer pod wrote. See decision record 0007.
  - `error_code` string, nullable: Machine-readable code (an APIErrorCode value, e.g. GENERATION_AI_INVALID_RESPONSE) for the terminal failure. Only populated when status=failed. Pair with `error` for fallback rendering.
  - `error` string, nullable: Default human-readable message for the terminal failure, matching `error_code`. Show as-is if the client doesn't recognise the code.
  - `retry_count` integer: Number of autoretries the worker has performed for this task.
  - `max_retries` integer, required: Maximum number of autoretries the worker will perform before giving up. Combined with retry_count, lets the client show an 'attempt N of M' progress indicator while the worker is in a retry chain.
  - `last_error_code` string, nullable: Machine-readable code for the most recent transient failure while the task is still retrying. Distinct from `error_code`, which is set only on terminal failure.
  - `last_error` string, nullable: Default human-readable message for the most recent transient failure while still retrying. Pair with `last_error_code`.

## Other responses

- `400`: Validation error or domain rule violation.
- `401`: Missing, expired, or invalid authentication.
- `403`: Authenticated but not permitted.
- `404`: Resource not found.
- `409`: Resource state conflict (duplicate, illegal transition).
- `422`: Request body failed schema validation.
- `500`: Server-side failure.

---

[API](https://skmtc.dev/tokumalabs/apis/tokuma-user-api.md) - [All operations](https://skmtc.dev/tokumalabs/apis/tokuma-user-api/llms.txt) - [OpenAPI document](https://skmtc.dev/tokumalabs/apis/tokuma-user-api/revisions/1f8201af459b?raw)
