Work entries
A work entry records human work: a duration in minutes against a project, on a calendar day, with a short description and optional note and tags. Entries are scoped to a workspace and authored by the calling user.
entryDate is a local date in the author’s timezone, frozen at write. When omitted on
create, it defaults to today in the author’s timezone. See
Dates and time.
The work entry object
Section titled “The work entry object”| Field | Type | Description |
|---|---|---|
id |
string | Unique identifier. |
workspaceId |
string | Owning workspace. |
projectId |
string | null | Assigned project, or null if its project was deleted. |
userId |
string | Author. |
entryDate |
string | Local date YYYY-MM-DD in the author’s timezone. |
durationMinutes |
integer | Minutes worked (positive). |
startedAt |
string | null | Optional ISO 8601 start instant. |
endedAt |
string | null | Optional ISO 8601 end instant. |
description |
string | Short summary (1–2000 chars). |
note |
string | null | Optional long-form note (≤ 10000 chars). |
tags |
string[] | Up to 20 tags (≤ 50 chars each). |
source |
string | Channel it was created through: web, cli, mcp, or api. Server-set. |
createdAt |
string | ISO 8601 creation instant. |
updatedAt |
string | ISO 8601 last-update instant. |
List work entries
Section titled “List work entries”GET /api/v1/work-entries scope: read
Returns entries in a workspace, newest first. Project ACL applies: an entry assigned to a project is visible to workspace admins, the author, and members of that project.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
workspaceId |
string | yes | Workspace to list from. |
projectId |
string | no | Filter to a single project. |
userId |
string | no | Filter to a single author. |
tag |
string | no | Filter to entries carrying this tag. |
from |
string | no | Inclusive start date YYYY-MM-DD. |
to |
string | no | Inclusive end date YYYY-MM-DD. |
limit |
integer | no | Page size, 1–200 (default 50). |
offset |
integer | no | Rows to skip (default 0). |
Create a work entry
Section titled “Create a work entry”POST /api/v1/work-entries scope: write
Creates an entry authored by the caller. The caller must be a member of the workspace and have access to the target project. Rate-limited per credential.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
workspaceId |
string | yes | Target workspace. |
projectId |
string | yes | Target project (must belong to the workspace). |
durationMinutes |
integer | yes | Minutes worked (positive). |
description |
string | yes | Short summary (1–2000 chars). |
entryDate |
string | no | Local date YYYY-MM-DD. Defaults to today in the author’s timezone. |
startedAt |
string | no | ISO 8601 start instant. |
endedAt |
string | no | ISO 8601 end instant. |
note |
string | no | Long-form note (≤ 10000 chars). |
tags |
string[] | no | Up to 20 tags. Defaults to []. |
agentEntryIds |
string[] | no | Agent sessions to link to this entry (the ones it was logged from). Only on the single-entry create, not the bulk one. |
Returns 201 Created. CLI and MCP clients set X-Spantail-Client: cli|mcp so the entry’s
source reflects the channel; direct callers default to api.
Create work entries in bulk
Section titled “Create work entries in bulk”POST /api/v1/work-entries/batch scope: write
Bulk-inserts up to 100 entries for one workspace in a single atomic request — either
every entry is written or none is (a single invalid entry rejects the whole batch with
400). Built for migrating data from an existing system; the
CLI’s entries import reads a JSONL file and auto-splits it into
requests of this size. Rate-limited per credential: one request consumes one rate-limit
token regardless of entry count.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
workspaceId |
string | yes | Target workspace for every entry in the batch. |
entries |
object[] | yes | 1–100 entries, spanning at most 10 distinct projects. |
Each entry takes the same fields as single create, with these differences:
workspaceIdis lifted to the top level and omitted per entry.entryDateis required — migrated data must state its dates explicitly; there is no default-to-today.externalId(string, optional) — an idempotency key, see below.
externalId semantics
Normally leave externalId out. Set it when you need to keep the source system’s id — for
resuming a partial migration safely, or for an external integration that must map its
records to Spantail entries.
- The
externalIdbecomes the entry’sid, so the old id addresses the entry directly (GET /api/v1/work-entries/:id). - Re-sending an entry with the same
externalIdupdates it in place instead of duplicating it. Entries without one are plain inserts and duplicate on re-send. - It must be unique across the instance (it is a primary key). If a source system’s ids are
weak (e.g. bare numbers), prefix them:
legacy-123. - Allowed charset:
A-Z a-z 0-9 . _ : -, at most 200 characters (it appears in URLs). The literal valuesstats,tags,batch,., and..are reserved — the first three are route segments under/work-entries, and the dot-segments normalize away in URLs. - Duplicate
externalIdvalues within one request are rejected with400. AnexternalIdthat already belongs to another user’s or another workspace’s entry is rejected with409and nothing is written.
The entry’s calendar day is taken verbatim from entryDate — the importer performs no
timezone conversion.
Returns 201 Created with the number of entries processed (created or updated).
Get a work entry
Section titled “Get a work entry”GET /api/v1/work-entries/:id scope: read
Returns a single entry. Subject to the same project ACL as listing — a hidden entry returns
404.
Get an entry’s linked agent sessions
Section titled “Get an entry’s linked agent sessions”GET /api/v1/work-entries/:id/agent-entries scope: read
Returns the agent sessions linked to this entry — the ones it was logged from. Same read access as the entry itself. Gated by the instance’s AI agents feature: when that is off, the route is unavailable.
Update a work entry
Section titled “Update a work entry”PATCH /api/v1/work-entries/:id scope: write
Only the author may edit an entry. All body fields are optional; send just what changes. Fields accept the same constraints as on create.
Delete a work entry
Section titled “Delete a work entry”DELETE /api/v1/work-entries/:id scope: write
Only the author may delete an entry. Returns 204 No Content.
GET /api/v1/work-entries/stats scope: read
Returns aggregated totals for the same filter set as listing (workspaceId required; plus
projectId, userId, tag, from, to). No pagination.
byProject may include a null projectId bucket for entries whose project was deleted.
Only dates that have entries appear in byDate; clients zero-fill gaps.
GET /api/v1/work-entries/tags scope: read
Returns the distinct tags in scope, for populating a filter dropdown. Accepts workspaceId
(required) and optional projectId.