Skip to main content

Task lifecycle

Every task in AfriAnnotate carries a state that tracks where it is in the labelling workflow. Three states:

States are managed by the FSM (finite-state machine) module under label_studio/fsm/ and are feature-flag gated — when the flag is off no state records are written. They live in their own TaskState records keyed by UUID7 (time-ordered) — distinct from the Task row, so state history is preserved.

The three states​

CREATED​

The task has been imported but has no annotation yet. Default state for every new task. Opening the task, saving a draft, or commenting does not change it.

COMPLETED​

Set when the first annotation is submitted on the task. This is independent of the project's maximum annotations per task: the FSM state says "someone has submitted", while is_labeled (the flag the labelling stream and Data Manager use) says "the task reached its annotation target".

IN_PROGRESS​

Entered only when annotations are deleted: after a deletion the task's state is re-derived from is_labeled — if the task no longer meets its annotation target it moves COMPLETED → IN_PROGRESS (for example, all annotations removed, or one of three removed on a three-annotator project). The next submitted annotation moves it back to COMPLETED.

For tasks that predate the FSM, state is inferred on first touch: is_labeled → COMPLETED, some annotations but not labelled → IN_PROGRESS, none → CREATED.

The annotation cap is enforced at write time​

Rotation stops serving a full task, but the cap is also a wall at annotation create: once a task has its target number of annotations, a direct submit — Data Manager quick-view, API client, replayed offline queue — is rejected with 403 task_fully_annotated ("This task already has its target number of annotations — move on to the next one"). Two exemptions: a user still holding the task's lock may submit (rotation served them the task before it filled; their in-flight work is never lost to the race), and managers, staff, and reviewers may annotate past the cap — adjudication adds annotations beyond it by design. Skips don't count against the cap and are always allowed.

Annotation state​

Annotations have their own state but in the current schema it's just CREATED — annotations are append-only artifacts. Submissions, updates, and deletes happen on the annotation itself; the AnnotationState table exists for history tracking, not gating.

Project state​

Projects also carry state, derived from their tasks:

  • CREATED — no task has been completed yet
  • IN_PROGRESS — at least one task is COMPLETED
  • COMPLETED — every task is COMPLETED; drops back to IN_PROGRESS when any task leaves COMPLETED (e.g. annotations deleted)

Transitions are automatic. There is no archive action — pausing a project (unpublishing it) is a separate setting and does not touch FSM state.

Data Manager​

The Data Manager does not show FSM state as a column. Use its real columns instead:

  • Annotations = 0 — nothing submitted yet
  • Completed (timestamp) is empty — the task has not reached its annotation target
  • Cancelled > 0 — skipped at least once

Where state changes are recorded​

Every transition lands in the TaskState / ProjectState tables with the transition name, reason, and user — queryable through the FSM API. Transitions do not write platform audit-log events or fire webhooks on their own.

GET {{platformURL}}/api/fsm/entities/<task|annotation|project>/<id>/history

Returns the entity's state records newest-first, paginated (page, page_size — default 100, max 1000): id (UUID7), state, previous_state, transition_name, triggered_by (id, email), reason, context_data, created_at. Filters: state, previous_state, transition_name, triggered_by=<user id>, created_at_from / created_at_to (ISO 8601); ordering=id / -id only. Needs view permission on the entity; an entity the caller can't see returns 404 (not 403), and results are limited to the caller's active organisation. Annotator-role callers get the same list without triggered_by. Unknown entity types are 404. With the FSM feature flag off the list is simply empty — nothing was ever written.

Manual overrides​

There is no UI for forcing a state. Registered manual transitions can be executed through the API:

POST {{platformURL}}/api/fsm/entities/<entity_type>/<entity_id>/transition/

with {"transition_name": "...", "transition_data": {...}} (data optional). Only transitions that are not auto-triggered are accepted: task_completed, task_in_progress, project_in_progress, project_completed, project_in_progress_from_completed. The *_created and annotation_updated transitions fire on model changes and return 400 here, as do unknown names and transitions whose own validation fails. Needs change permission on the entity (404 when not visible). Response: {"success": true, "new_state": ..., "state_record": {...}}; with the feature flag off the call still succeeds with new_state and state_record null and writes nothing. Useful for unsticking state after a workflow change.

The FSM framework (developer-facing)​

The FSM module is reusable — you can define your own entity states + transitions if you're extending the platform:

  • State models (fsm/state_models.py): BaseState, TaskState, AnnotationState, ProjectState — UUID7-keyed state records
  • Transition system (fsm/transitions.py): declarative pydantic-based transitions with validation
  • Registry (fsm/registry.py): register entity types and transitions
  • TransitionExecutor (fsm/transition_executor.py): runs the state change + post-transition hooks (e.g. task → project updates)

See label_studio/fsm/README.md in the repo for the developer guide.

What's next​