Skip to content

Lifecycle States

Every node in Portuni has two layers of state:

  1. status – a coarse, system-wide enum: active, completed, archived. This is what filters and queries lean on.
  2. lifecycle_state – a fine-grained, type-specific enum that says where in its life the node currently is. This is what humans and agents actually look at.

status is derived from lifecycle_state by a database trigger. You set the lifecycle state; the system computes the status. This keeps queries fast (one indexed enum) and the human-visible state expressive (different vocabularies for different node types).

A project goes through kickoff -> planning -> operating -> done. A process doesn’t – processes don’t have “kickoff,” they have “draft -> active -> deprecated.” Forcing one set of states on every node type produces vague labels nobody trusts.

Instead, each type has its own state machine, with vocabulary that fits the type. The trigger maps each lifecycle state to the right coarse status, so a single WHERE status = 'active' query still works.

The exact enums live in src/popp.ts – the single source of truth shared by the backend and the frontend. The general shape:

StateCoarse statusMeaning
kickoffactiveJust started – scope and team being defined
planningactiveApproach decided, work being broken down
operatingactiveExecution is happening
donecompletedDelivered – no more active work expected
archivedarchivedClosed out, kept for reference
StateCoarse statusMeaning
draftactiveBeing written or reworked
operatingactiveIn active use
deprecatedactiveStill works, but a replacement exists
archivedarchivedNo longer used
StateCoarse statusMeaning
operatingactiveCurrently being managed
dormantactiveOwned but not actively worked on
archivedarchivedNo longer relevant

Principles are simple – they’re either active or archived. No intermediate states.

Organizations follow the same simple model: active or archived. Organizations don’t have a working life cycle in the same way projects do.

Lifecycle states map to one of four color buckets in the UI:

BucketMeaningExamples
GreenLive, operational, moving forwardactive, operating, in_progress, done
YellowWarning, waiting, in fluxneeds_attention, kickoff, planning, dormant
RedBlocked or problematic(reserved for future error states)
GrayDefault fallbackAnything not explicitly mapped

The mapping lives in app/src/types.ts (the LIFECYCLE_COLORS constant). Any state not listed falls through to gray, which is a safe default for new states added later.

Both portuni_create_node and portuni_update_node accept an optional lifecycle_state parameter. The Zod enum validates the value against the type’s allowed states – attempting to set a project to deprecated (which only processes have) is rejected before hitting the database.

Defaults at creation time:

TypeDefault lifecycle state
projectkickoff
processdraft
areaoperating
principleactive
organizationactive

The status field could be set in the tool layer alongside lifecycle_state, but a trigger is more robust. Direct SQL updates, future REST endpoints, seed scripts – all of them go through the same trigger and stay consistent. The application code stops needing to remember the mapping.

Most queries filter by status (the coarse enum) because that’s what indexes well and what most callers actually mean. Show me the active projects: WHERE type = 'project' AND status = 'active'. The fine-grained lifecycle_state is for display and for state-machine transitions, not for bulk filtering.

When you do want to filter by lifecycle state – say, “all projects in planning” – the query works the same way; there just isn’t a dedicated index, so it scans more.