Skip to content

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.

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.

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).
curl "https://<your-instance>/api/v1/work-entries?workspaceId=wrk_demo&from=2026-06-01&to=2026-06-30" \
  -H "Authorization: Bearer spantail_pat_yourtoken"

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.
curl -X POST "https://<your-instance>/api/v1/work-entries" \
  -H "Authorization: Bearer spantail_pat_yourtoken" \
  -H "Content-Type: application/json" \
  -d '{
    "workspaceId": "wrk_demo",
    "projectId": "prj_site",
    "durationMinutes": 90,
    "description": "Reviewed onboarding flow",
    "tags": ["review"]
  }'

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.

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:

  • workspaceId is lifted to the top level and omitted per entry.
  • entryDate is 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 externalId becomes the entry’s id, so the old id addresses the entry directly (GET /api/v1/work-entries/:id).
  • Re-sending an entry with the same externalId updates 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 values stats, tags, batch, ., and .. are reserved — the first three are route segments under /work-entries, and the dot-segments normalize away in URLs.
  • Duplicate externalId values within one request are rejected with 400. An externalId that already belongs to another user’s or another workspace’s entry is rejected with 409 and nothing is written.

The entry’s calendar day is taken verbatim from entryDate — the importer performs no timezone conversion.

curl -X POST "https://<your-instance>/api/v1/work-entries/batch" \
  -H "Authorization: Bearer spantail_pat_yourtoken" \
  -H "Content-Type: application/json" \
  -d '{
    "workspaceId": "wrk_demo",
    "entries": [
      {
        "projectId": "prj_site",
        "entryDate": "2024-07-15",
        "durationMinutes": 90,
        "description": "Reviewed onboarding flow",
        "externalId": "legacy-4711"
      },
      {
        "projectId": "prj_site",
        "entryDate": "2024-07-16",
        "durationMinutes": 60,
        "description": "Fixed signup validation"
      }
    ]
  }'

Returns 201 Created with the number of entries processed (created or updated).

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 /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.

curl "https://<your-instance>/api/v1/work-entries/we_01/agent-entries" \
  -H "Authorization: Bearer spantail_pat_yourtoken"

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.

curl -X PATCH "https://<your-instance>/api/v1/work-entries/we_01J8" \
  -H "Authorization: Bearer spantail_pat_yourtoken" \
  -H "Content-Type: application/json" \
  -d '{ "durationMinutes": 120, "note": "Extended to cover edge cases" }'

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.

{
  "totalMinutes": 1830,
  "entryCount": 24,
  "byDate": [{ "date": "2026-06-28", "minutes": 90, "count": 1 }],
  "byProject": [{ "projectId": "prj_site", "minutes": 1200, "count": 16 }],
  "byUser": [{ "userId": "usr_ana", "minutes": 1830, "count": 24 }]
}

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.

["review", "meeting", "bugfix"]