time-entries

Generate Time Entries

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.

post/v1/time-entries/generate

Request

  • Base URL
    https://app.tokumalabs.com/api
  • Auth
    OAuth 2
    Password
    Token URL: v1/auth/token

Request body

datestring 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

Successful Response

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_idstring nullable

Unique task identifier. Null only when outcome is skipped — there is no run to watch.

datestring date required

The date this generation targets

statusstring 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_codestring 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.

errorstring nullable

Default human-readable message for the terminal failure, matching error_code. Show as-is if the client doesn't recognise the code.

retry_countinteger

Number of autoretries the worker has performed for this task.

max_retriesinteger 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_codestring 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_errorstring nullable

Default human-readable message for the most recent transient failure while still retrying. Pair with last_error_code.

Changes

No recorded changes to this operation across all 1 revision of this API.