> For AI agents: the complete documentation index is available at /llms.txt, the full documentation bundle is available at /llms-full.txt.

> **Themelio compatibility reference.** This page describes the retained Themelio implementation or its historical design. Check the [parallel-operation guide](/compatibility/index.md) before applying it to the current platform.

# ADR-0003: Waldur Proposal Workflow

**Status:** Proposed

**Date:** 2026-05-07

**Deciders:** TBD

**Relates to:** [ADR-0000 (Friends & Family MVP)](/adr/0000-mvp-friends-and-family.md) — "Proposal Workflow" exploration item; Waldur Installation & Management (future ADR) — functional requirements FR-PRO-01 through FR-PRO-04.

## Context

The Themelio platform currently has a 5-step project wizard (`/projects/new`) that collects project metadata, PI/organization details, scientific rationale, and resource requests. However, the entire proposal lifecycle is **client-side only** — drafts and submissions are stored in `localStorage` via `projectsStore.ts`, and no data reaches the backend. The form data goes nowhere.

The [Friends & Family MVP (ADR-0000)](/adr/0000-mvp-friends-and-family.md) explicitly defers the proposal workflow as a post-MVP item:

> The resource request process will be connected to Waldur's proposal management system, replacing the current browser-only prototype.

Meanwhile, Waldur provides a full Proposals API (`/api/proposal-proposals/`) with a formal review workflow (Draft → Submitted → In Review → Accepted/Rejected), and the existing `waldur-k8s-site-agent` already provisions Kubernetes namespaces and GitLab groups when Waldur orders are approved. However, this integration only works in the **Waldur → Themelio** direction (Waldur pushes approved orders to the site-agent). There is no **Themelio → Waldur** path for users to submit proposals.

Integrating with the Waldur Proposals API would:

1. Make proposals **persistent and shared** — admins can review them; users see status across devices.
2. Connect proposals to the existing **Waldur order and provisioning pipeline** — approved proposals can trigger resource allocation via the site agents already deployed (`waldur-k8s-site-agent`, GitLab sync via `gitlab-root-actor`).
3. Preserve the **rejection/resubmission UX** that Themelio requires but Waldur's project model does not natively support.

### Key Constraints

1. **Waldur proposals are terminal on rejection** — the API does not support resubmission of a rejected proposal. A new proposal must be created.
2. **Users should not context-switch to Waldur** — the ProjectWizard should be the single interface for proposal submission.
3. **Waldur proposals have no generic attributes field** — the Proposal model uses structured fields only (no JSONField). Themelio-specific fields that don't map to Waldur's schema need an alternative store.

### Current architecture gaps

- **No backend for proposals.** The web service has no API route or service for proposal persistence.
- **No Waldur API client in the web layer.** The Waldur API token is a privileged credential; it currently lives only in the site-agent deployment configs.
- **Field mismatch.** The Themelio form captures 7 scientific rationale fields, rich resource breakdowns (HPC partitions, cloud quotas, platform services), and team member details that don't map 1:1 to Waldur's proposal schema.
- **Rejection semantics differ.** Waldur hides rejected projects from the requesting user. Themelio must show rejected proposals and allow editing/resubmission.

## Decision

### 1. Introduce a dedicated `waldur-root-actor` service

A new Fastify service (following the pattern of `gitlab-root-actor` and `k8s-root-actor`) that acts as the sole intermediary between Themelio and the Waldur Proposals API. It holds the Waldur API token and exposes an internal REST API consumed by the web service.

```
┌──────────────┐      ┌─────────────────────┐      ┌──────────────────┐
│  Themelio Web │─────►│  waldur-root-actor   │─────►│  Waldur API      │
│  (Next.js)   │ HTTP │  (Fastify, port 3003)│ HTTP │  (dev/staging)   │
└──────────────┘      └─────────────────────┘      └──────────────────┘
```

**Why a dedicated service:**

- **Credential isolation.** The Waldur API token stays out of the web service, matching the existing pattern where `gitlab-root-actor` holds the GitLab root token and `k8s-root-actor` holds K8s credentials.
- **Single responsibility.** Proposal lifecycle logic (field mapping, resubmission lineage, status polling) lives in one place.
- **Testability.** The service can be tested against Waldur's dev instance independently of the web frontend.

:::note Alternative considered
Extending `gitlab-root-actor` instead of creating a new service. This is viable since it already contains Waldur sync logic (`waldur-sync.ts`). If this path is chosen, the service should be renamed to something like `platform-root-actor` to reflect its broader scope. The ADR recommends starting with a dedicated service to keep the blast radius small, with consolidation as a future option.
:::

### 2. Use Waldur as the primary draft store; GitLab group for overflow fields only

On first draft save, `waldur-root-actor` creates a **Waldur proposal in `draft` state** (`POST /api/proposal-proposals/`). The Waldur Proposals API natively supports drafts — proposals start in `DRAFT` state and can be updated via `POST /api/proposal-proposals/{uuid}/update_project_details/` before submission. This makes Waldur the primary persistence layer from the first save, not just after submission.

**What lives in Waldur (structured fields):**

| Themelio field       | Waldur field                                     |
| -------------------- | ------------------------------------------------ |
| `projectTitle`       | `name`                                           |
| `scientificSummary`  | `project_summary`                                |
| `civilianPurpose`    | `project_has_civilian_purpose`                   |
| `confidentiality`    | `project_is_confidential`                        |
| `duration`           | `duration_in_days`                               |
| Resources            | `RequestedResource` (via `/resources/` endpoint) |
| Scientific rationale | Checklist answers (via `/submit_answers/`)       |
| Team members         | Proposal users (via `/add_user/`)                |

**What lives in GitLab custom attributes (overflow — fields with no Waldur equivalent):**

| Key                        | Value                                                                                                                       | Size                       |
| -------------------------- | --------------------------------------------------------------------------------------------------------------------------- | -------------------------- |
| `waldur_proposal_uuid`     | Waldur proposal UUID                                                                                                        | \~36 bytes                 |
| `waldur_proposal_overflow` | JSON: `{acronym, tags, piGender, piJobTitle, piPermanentContract, platformServices: {kubeflow, mlflow, openOnDemand, ssh}}` | \~200-500 bytes            |
| `waldur_proposal_history`  | JSON array of past submission UUIDs, timestamps, and outcomes (for resubmission lineage)                                    | \~100 bytes per submission |

A GitLab group is still pre-created alongside the Waldur draft to store these overflow fields. However, the group is much lighter — it holds only Themelio-specific metadata (\~500 bytes total) rather than the full proposal data blob (\~2-5 KB).

:::warning GitLab group naming — needs discussion
The project wizard currently collects `projectTitle` and `acronym` but no explicit slug or path identifier. When a GitLab group is pre-created at draft time, it needs a path. Options include deriving it from the acronym, generating a UUID-based path, or introducing an explicit project slug field in the wizard. This naming decision also affects what happens if the user changes the project title between draft and submission. To be resolved before implementation.
:::

When Waldur approves the proposal and the site-agent provisions resources, it creates subgroups (models/, datasets/, spaces/, etc.) **inside this pre-existing group** — no data migration needed. The draft group becomes the project group.

This approach means proposals are server-side persisted from the first save, visible across devices, and survive browser state loss. The `localStorage` draft in the wizard is retained only as a transient editing buffer.

### 3. On form submission, finalize and submit the existing Waldur draft

The Waldur proposal already exists in `DRAFT` state (created in Section 2). When the user clicks "Submit for Review", the web service calls `waldur-root-actor`, which:

1. Updates the draft with final field values (`POST /api/proposal-proposals/{uuid}/update_project_details/`)
2. Attaches resource requests (`POST /api/proposal-proposals/{uuid}/resources/`)
3. Submits checklist answers for scientific rationale fields (`POST /api/proposal-proposals/{uuid}/submit_answers/`)
4. Adds team members as proposal users (`POST /api/proposal-proposals/{uuid}/add_user/`)
5. Submits the proposal (`POST /api/proposal-proposals/{uuid}/submit/`)
6. Updates the GitLab group overflow custom attributes if changed

### 4. Calls and Rounds are admin-provisioned in Waldur

The admin creates Calls for Proposals and Rounds directly in the Waldur UI (e.g. "Daedalus 2026 H2 — National Call"). Themelio fetches available open rounds via `GET /api/call-rounds/` and presents them in the project wizard (Step 1 or as a pre-step).

Themelio does **not** create or manage Calls/Rounds programmatically. This matches the existing pattern where offerings and service providers are set up by operators, not through Themelio.

### 5. Store scientific rationale in Waldur checklists

The Themelio form's scientific rationale fields (7 structured fields) do not have direct Waldur equivalents. The recommended approach is to use **Waldur's checklist system**:

- A checklist template is pre-configured in Waldur (admin-provisioned alongside the Call) with questions mapping to each scientific rationale field.
- On submission, `waldur-root-actor` calls `POST /api/proposal-proposals/{uuid}/submit_answers/` with the structured responses.
- The checklist approach preserves data in a queryable, reviewable format within Waldur's UI.

| Themelio field              | Waldur checklist question         |
| --------------------------- | --------------------------------- |
| `scientificCaseDescription` | Scientific case description       |
| `aiMlTechnologies`          | AI/ML technologies used           |
| `algorithmsAndMethods`      | Algorithms and methods            |
| `codeDescription`           | Code and software description     |
| `currentBottlenecks`        | Current computational bottlenecks |
| `scalabilityTarget`         | Scalability targets               |
| `optimizationWorkNeeded`    | Optimization work needed          |

:::note
The Waldur Proposal model has no generic `attributes` JSONField, so the checklist system is the only structured way to store scientific rationale data in Waldur. This is not a tradeoff — it's the required approach.
:::

### 6. Map resource requests to Waldur proposal resources

Each enabled resource type in the form becomes a separate proposal resource linked to a pre-configured offering:

**HPC Resources → SLURM Offering**

```json
{
  "requested_offering": "<slurm-offering-uuid>",
  "limits": {
    "node_hours": 250000
  },
  "attributes": {
    "partition": "gpu-h100"
  },
  "description": "HPC resource request"
}
```

**Cloud Resources → Kubernetes Offering**

```json
{
  "requested_offering": "<k8s-offering-uuid>",
  "limits": {
    "cpu": 32,
    "ram": 128,
    "storage": 2000
  },
  "description": "Cloud resource request — <cloudJustification>"
}
```

**Platform services** (Kubeflow, MLflow, Open OnDemand, SSH) are stored as boolean attributes on the Cloud resource request or as a separate platform offering resource, depending on how the Waldur offerings are structured by the admin.

### 7. Field mapping: Themelio form → Waldur Proposals API

#### Proposal creation (`POST /api/proposal-proposals/`)

| Themelio field               | Waldur field                   | Notes                                              |
| ---------------------------- | ------------------------------ | -------------------------------------------------- |
| `overview.projectTitle`      | `name`                         | Direct                                             |
| `overview.scientificSummary` | `project_summary`              | Direct                                             |
| `overview.civilianPurpose`   | `project_has_civilian_purpose` | Direct                                             |
| `overview.confidentiality`   | `project_is_confidential`      | Direct                                             |
| `overview.duration`          | `duration_in_days`             | Convert months → days                              |
| `overview.acronym`           | —                              | GitLab group overflow custom attributes            |
| `overview.startDate`         | —                              | GitLab group overflow; effective date set by admin |
| `overview.tags`              | —                              | GitLab group overflow custom attributes            |
| (selected round)             | `round_uuid`                   | From available rounds                              |

#### Organization and PI (`add_user` + `update_project_details`)

| Themelio field                  | Waldur mapping                                             |
| ------------------------------- | ---------------------------------------------------------- |
| `organization.piFullName`       | Proposal creator (authenticated user)                      |
| `organization.piEmail`          | Applicant email (from auth)                                |
| `organization.piJobTitle`       | `applicant_job_title` on proposal                          |
| `organization.organizationName` | `applicant_organization` on proposal                       |
| `organization.teamMembers[*]`   | `POST /api/proposal-proposals/{uuid}/add_user/` per member |

#### Scientific rationale → Checklist answers

See Section 5 above.

#### Resources → Proposal resources

See Section 6 above.

#### Fields with no Waldur equivalent (stored in GitLab group overflow)

| Themelio field                                      | Storage                                         |
| --------------------------------------------------- | ----------------------------------------------- |
| `acronym`                                           | `waldur_proposal_overflow` JSON on GitLab group |
| `tags`                                              | `waldur_proposal_overflow` JSON on GitLab group |
| `startDate`                                         | `waldur_proposal_overflow` JSON on GitLab group |
| PI metadata (gender, job title, permanent contract) | `waldur_proposal_overflow` JSON on GitLab group |
| Platform services (Kubeflow, MLflow, etc.)          | `waldur_proposal_overflow` JSON on GitLab group |

### 8. Handle rejection and resubmission via new-proposal lineage

:::warning Needs attention
This area requires careful design and validation against the Waldur API during implementation. The resubmission flow described below is a recommended approach, but edge cases around proposal state transitions, round deadlines, and partial resubmissions need to be tested against the actual Waldur instance.
:::

Waldur does not support resubmitting a rejected proposal. To implement the Themelio requirement (user sees rejection, edits, resubmits), the following approach is used:

1. **On rejection:** `waldur-root-actor` detects the state change (via polling or on-demand fetch). The proposal's `REJECTED` state is read directly from Waldur; Themelio presents it as "Needs Revision".
2. **User edits and resubmits:** The web frontend pre-fills the form from the rejected Waldur proposal's structured fields (via `GET /api/proposal-proposals/{uuid}/`) plus the overflow data from GitLab custom attributes. On resubmission, `waldur-root-actor` creates a **new** Waldur proposal in `DRAFT` state, with a `description` noting the lineage (e.g., "Resubmission of proposal \{old-uuid}, sequence #2").
3. **Lineage tracking:** The old proposal UUID is appended to `waldur_proposal_history` on the GitLab group. The `waldur_proposal_uuid` attribute is updated to the new proposal UUID. The user sees a continuous history in Themelio, even though Waldur treats each as a separate proposal.
4. **Admin visibility:** The lineage note in the proposal `description` provides context to reviewers in Waldur that this is a revised submission.

From the user's perspective, this is a single continuous flow. From Waldur's perspective, each submission is a fresh proposal.

```mermaid
sequenceDiagram
    participant U as User Browser
    participant W as Themelio Web
    participant WRA as waldur-root-actor
    participant GL as GitLab
    participant API as Waldur API

    U->>W: View proposal (sees "Needs Revision")
    W->>WRA: GET /proposals/:id
    WRA->>API: GET /api/proposal-proposals/{uuid}/
    API-->>WRA: 200 {uuid, state: "rejected", name, ...}
    WRA->>GL: Read overflow custom attributes
    WRA-->>W: 200 {status: "needs_revision", data, history}
    W-->>U: Pre-filled form with rejection status

    U->>W: Edit and resubmit
    W->>WRA: POST /proposals/:id/resubmit
    Note over WRA: Creates NEW Waldur draft proposal<br/>with lineage note in description
    WRA->>API: POST /api/proposal-proposals/
    API-->>WRA: 201 {new_uuid, state: "draft"}

    Note over WRA: Attach resources, checklist,<br/>members (same as initial submit)

    WRA->>API: POST /api/proposal-proposals/{new_uuid}/submit/
    API-->>WRA: 200 {state: "submitted"}
    WRA->>GL: Update overflow attributes<br/>(new uuid, append old uuid to history)
    WRA-->>W: 200 {proposalId, status: "submitted"}
```

### 9. Poll for Waldur state changes

Waldur uses STOMP message queues for event notifications, but proposal state change events are not confirmed to be covered. Since proposal review is a human-timescale process (hours to days), **polling** `GET /api/proposal-proposals/?state=...` every 5-10 minutes is sufficient and simpler than maintaining a persistent WebSocket/STOMP connection.

**Load estimate:** Even with 500 active proposals, a single poll request every 5 minutes produces \~0.003 RPS against the Waldur API — negligible. The poller fetches only proposals with state changes since the last poll (filtered by `modified` timestamp), keeping response payloads small.

Two sync mechanisms are used:

- **On-demand fetch:** When a user views their proposals list or a proposal detail page, the web service calls `waldur-root-actor`, which queries Waldur directly for current status. Since Waldur is the source of truth, no local status cache is needed.
- **Background polling:** `waldur-root-actor` periodically polls for status changes to trigger side effects (e.g., sending notifications on approval/rejection). This can start as a simple interval and be upgraded to STOMP subscription later if Waldur confirms proposal events are published.

### 10. Sequence: proposal submission

```mermaid
sequenceDiagram
    participant U as User Browser
    participant W as Themelio Web
    participant WRA as waldur-root-actor
    participant GL as GitLab
    participant API as Waldur API

    U->>W: Save draft
    W->>WRA: POST /proposals/draft
    WRA->>API: POST /api/proposal-proposals/
    API-->>WRA: 201 {uuid, state: "draft"}
    WRA->>GL: Create GitLab group + overflow attributes
    WRA-->>W: 201 {draftId, waldurUuid}

    U->>W: Update draft (auto-save)
    W->>WRA: PUT /proposals/:id/draft
    WRA->>API: POST /api/proposal-proposals/{uuid}/update_project_details/
    WRA->>GL: Update overflow attributes (if changed)
    WRA-->>W: 200

    U->>W: Submit for review
    W->>WRA: POST /proposals/:id/submit
    Note over WRA: Validate & finalize draft

    WRA->>API: POST /api/proposal-proposals/{uuid}/update_project_details/
    Note over WRA: Final field sync

    par Attach resources
        WRA->>API: POST /api/proposal-proposals/{uuid}/resources/
        Note right of API: HPC resource
        WRA->>API: POST /api/proposal-proposals/{uuid}/resources/
        Note right of API: Cloud resource (if enabled)
    and Submit checklist
        WRA->>API: POST /api/proposal-proposals/{uuid}/submit_answers/
        Note right of API: Scientific rationale
    and Add team members
        WRA->>API: POST /api/proposal-proposals/{uuid}/add_user/
        Note right of API: Per team member
    end

    WRA->>API: POST /api/proposal-proposals/{uuid}/submit/
    API-->>WRA: 200 {state: "submitted"}

    WRA->>GL: Update overflow attributes (if needed)
    WRA-->>W: 200 {proposalId, status: "submitted"}
    W-->>U: Redirect to /projects/proposal/{id}
```

### 11. Error handling and partial failure

The submission sequence (Section 10) involves multiple Waldur API calls. Since the Waldur draft already exists, failures during submission are recoverable:

1. **Draft update fails** (`update_project_details`): The draft exists in Waldur with stale data. Return an error; the user can retry. No data is lost.
2. **Resource attachment or checklist submission fails**: The Waldur proposal exists but is incomplete. On retry (`POST /proposals/:id/submit`), `waldur-root-actor` queries the proposal's current resources and checklist state, skips already-completed steps, and retries only the failed ones.
3. **Final submit call fails** (`POST .../submit/`): All data is attached but the proposal remains in Waldur `DRAFT` state. The service returns an error; the user can retry submission without data loss.
4. **GitLab overflow attribute update fails after successful Waldur submission**: The overflow metadata is stale but all primary proposal data is safely in Waldur. The next on-demand fetch will reconcile.
5. **Initial draft creation fails** (`POST /api/proposal-proposals/`): No Waldur state exists yet. Return an error; the wizard retains the form data in `localStorage` as a fallback. The user can retry saving the draft.

The general principle: **Waldur is the source of truth for all proposal data; GitLab custom attributes store only overflow fields.** Any divergence between the two is resolved by re-reading Waldur state.

### 12. Sequence: proposal approval → resource provisioning

```mermaid
sequenceDiagram
    participant Admin as Admin
    participant API as Waldur API
    participant SA as Site Agents
    participant GRA as gitlab-root-actor

    Admin->>API: POST /api/proposal-proposals/{uuid}/approve/
    Note over API: State → approved<br/>Waldur creates Project + Orders

    API->>SA: Order process trigger
    Note over SA: waldur-k8s-site-agent<br/>creates namespace + quotas

    SA->>GRA: POST /waldur/project_create
    Note over GRA: Detects pre-existing GitLab group<br/>(created at draft time for overflow)<br/>Creates subgroups inside it<br/>(models/, datasets/, spaces/)

    SA->>GRA: POST /waldur/add_project_member
    Note over GRA: Adds team members<br/>to GitLab groups
```

### 13. `waldur-root-actor` internal API

The service exposes the following endpoints, consumed only by the Themelio web service:

| Method | Endpoint                  | Success | Key errors                         | Description                                                       |
| ------ | ------------------------- | ------- | ---------------------------------- | ----------------------------------------------------------------- |
| `GET`  | `/rounds`                 | `200`   | `502` (Waldur unreachable)         | List open rounds available for submission                         |
| `POST` | `/proposals/draft`        | `201`   | `409` (already exists), `502`      | Create Waldur draft proposal + GitLab group for overflow          |
| `PUT`  | `/proposals/:id/draft`    | `200`   | `404` (unknown draft), `502`       | Update Waldur draft via `update_project_details` + overflow attrs |
| `POST` | `/proposals/:id/submit`   | `200`   | `404`, `422` (validation), `502`   | Finalize draft, attach resources/checklist/members, submit        |
| `GET`  | `/proposals`              | `200`   | `502`                              | List proposals for the authenticated user                         |
| `GET`  | `/proposals/:id`          | `200`   | `404`, `403`                       | Get proposal detail with Themelio-enriched status                 |
| `POST` | `/proposals/:id/resubmit` | `200`   | `404`, `409` (not rejected), `502` | Create a new proposal linked to a rejected one                    |
| `GET`  | `/proposals/:id/history`  | `200`   | `404`                              | Get resubmission lineage chain                                    |

All endpoints return `401` if the internal service token is missing or invalid.

### 14. Authentication and authorization

Following the pattern established by `gitlab-root-actor`, the `waldur-root-actor` uses a shared internal token for service-to-service authentication:

- The web service includes an `Authorization: Bearer <token>` header on every request to `waldur-root-actor`.
- The token is configured via the `WALDUR_ROOT_ACTOR_INTERNAL_TOKEN` environment variable (same pattern as `GITLAB_ROOT_ACTOR_INTERNAL_TOKEN`).
- A `requireAuth()` guard validates the token on every route, returning `401` if missing or invalid.

**User identity** is passed by the web service as a request parameter (e.g., user email or GitLab user ID). The web service is trusted to provide the correct identity because the internal token proves it is the legitimate caller. `waldur-root-actor` does **not** independently verify end-user sessions.

**Authorization** is scoped per endpoint:

- `GET /proposals` and `GET /proposals/:id` filter results to proposals owned by the requesting user (matched via `created_by` on the Waldur proposal).
- `PUT /proposals/:id/draft`, `POST /proposals/:id/submit`, and `POST /proposals/:id/resubmit` verify that the requesting user is the proposal owner before proceeding.
- `GET /rounds` is accessible to any authenticated caller (no per-user filtering).

### 15. Configuration

The service requires the following environment variables:

| Variable                           | Description                                                                                    |
| ---------------------------------- | ---------------------------------------------------------------------------------------------- |
| `WALDUR_API_URL`                   | Waldur instance base URL (e.g. `https://dev.waldur.pharos.grnet.gr/api/`)                      |
| `WALDUR_API_TOKEN`                 | Service account token for Waldur API                                                           |
| `WALDUR_HPC_OFFERING_UUID`         | UUID of the SLURM/HPC offering                                                                 |
| `WALDUR_K8S_OFFERING_UUID`         | UUID of the Kubernetes/Cloud offering                                                          |
| `WALDUR_ROOT_ACTOR_INTERNAL_TOKEN` | Shared secret for service-to-service auth (same pattern as `GITLAB_ROOT_ACTOR_INTERNAL_TOKEN`) |
| `PORT`                             | Service port (default: `3003`)                                                                 |

## Data Flow

```
User fills wizard → POST /proposals/draft (waldur-root-actor)
  → Creates Waldur proposal in DRAFT state (POST /api/proposal-proposals/)
  → Creates GitLab group for overflow fields (acronym, tags, platform services, etc.)
  → Returns draft ID (Waldur UUID) to UI

User saves progress → PUT /proposals/{id}/draft (waldur-root-actor)
  → Updates Waldur draft (POST .../update_project_details/)
  → Updates GitLab overflow attributes if changed

User clicks "Submit for Review"
  → POST /proposals/{id}/submit (waldur-root-actor)
  → Finalizes Waldur draft (POST .../update_project_details/)
    → Attaches resource requests (POST .../resources/) for HPC and K8s offerings
    → Submits checklist answers (POST .../submit_answers/) for scientific rationale
    → Adds team members (POST .../add_user/)
    → Submits proposal (POST .../submit/)

User views proposal status → GET /proposals/{id} (waldur-root-actor)
  → Reads state directly from Waldur (GET /api/proposal-proposals/{uuid}/)
  → Reads overflow fields from GitLab group custom attributes
  → Returns merged view

Poller detects state change in Waldur:
  → Approved: site-agent provisions subgroups inside existing GitLab group
  → Rejected: user sees "Needs Revision", edits, resubmits → new Waldur proposal, same GitLab group
```

## Open Items

### Blocking (must be resolved before implementation)

- **`round_uuid` resolution**: Creating a Waldur proposal (even in draft) requires a round UUID. Either configure a default round via env var, or dynamically fetch open rounds via `GET /api/call-rounds/` and present a selection in the wizard. The choice affects the wizard UX (Step 1 or pre-step). This is now blocking for draft creation, not just submission.
- **Offering UUIDs**: HPC and K8s offering UUIDs must be configured (`WALDUR_HPC_OFFERING_UUID`, `WALDUR_K8S_OFFERING_UUID`) before any resource attachment can work.
- **User UUID mapping**: Adding team members requires Waldur user UUIDs. Options: (a) lookup by email via `GET /api/users/?email=...`, or (b) store Waldur UUID during OIDC login. Option (a) is simpler but adds a Waldur API call per team member; option (b) requires changes to the auth flow.
- **GitLab group naming**: See the warning note in Section 2 — no slug/path field exists in the wizard today.
- **`update_project_details` field coverage**: Confirm which fields the `update_project_details` action accepts. If it only covers a subset of proposal fields, some data may need to be set at creation time only. To be validated against the Waldur dev instance.

### Non-blocking (can be resolved during or after implementation)

- **Checklist template sync**: The checklist template is configured per-call in Waldur. Themelio either fetches and renders it dynamically, or maintains a parallel form. Dynamic fetching is recommended to avoid drift.
- **Site-agent coordination**: The site-agent currently creates the GitLab project group. With pre-created groups, the site-agent must detect the existing group and create subgroups inside it rather than creating a new top-level group.
- **Orphaned drafts**: Creating Waldur proposals in draft state means abandoned drafts if a user never submits. Waldur drafts may need periodic cleanup (depends on whether Waldur handles this natively). The corresponding GitLab overflow groups should be cleaned up in tandem.

## Consequences

### Positive

- **Proposals become real.** The current localStorage-only system is replaced with persistent, server-side storage — drafts and submitted proposals in Waldur, overflow fields in GitLab custom attributes.
- **Waldur as single source of truth.** All primary proposal data (title, summary, resources, rationale, status) lives in Waldur from the first save. GitLab stores only Themelio-specific overflow (\~500 bytes), not a full data mirror.
- **No new database or infrastructure.** Waldur's native draft support and GitLab custom attributes cover all persistence needs.
- **End-to-end automation.** An approved proposal automatically triggers the existing site-agent pipeline (K8s namespace creation, GitLab group provisioning, member sync) without manual intervention.
- **Admin review in Waldur.** Admins can review proposals, checklists, and resource requests in Waldur's native UI without depending on Themelio for the review workflow.
- **Credential isolation.** The Waldur API token is confined to a single service, following established patterns.
- **Full proposal history.** Resubmission lineage is preserved in GitLab overflow attributes, giving users a continuous view even though Waldur treats each submission as separate.

### Negative

- **New service to deploy and maintain.** Adds operational overhead (one more Fastify service, health checks, deployment manifests).
- **Checklist template coupling.** The scientific rationale mapping depends on a pre-configured checklist template in Waldur. Template changes require coordinated updates in both Waldur and `waldur-root-actor`.
- **Resubmission complexity.** The lineage tracking for rejected/resubmitted proposals adds state (the history array) that lives in GitLab, outside Waldur. This is minimal (\~100 bytes per submission) but still a secondary store.
- **Orphaned drafts.** Creating Waldur proposals in draft state means abandoned drafts accumulate. The corresponding GitLab groups for overflow also become orphaned. Needs periodic cleanup in both systems.
- **Dual-store coordination.** Although GitLab stores only overflow fields, `waldur-root-actor` must keep the GitLab group and Waldur proposal in sync (e.g., updating `waldur_proposal_uuid` on resubmission). Failure to sync results in stale overflow data, though primary proposal data remains correct in Waldur.

### Risks

- **Waldur API stability.** The Proposals API is newer than the core Marketplace API. Breaking changes could require service updates.
- **Round availability.** If no open round exists when a user tries to submit, the form must handle this gracefully (disable submit, show message).
- **Checklist template drift.** If the Waldur checklist template and the Themelio form diverge, submissions could fail silently or lose data.

## Out of Scope

- **Waldur Call/Round management from Themelio.** Calls and Rounds are admin-provisioned directly in Waldur.
- **Review workflow in Themelio.** Proposal review and approval happen in Waldur's UI. Themelio only displays status.
- **Document attachments.** The Waldur API supports PDF attachments (`attach_document`). This can be added later if the scientific case requires uploaded documents.
- **STOMP-based status sync.** Initial implementation uses polling + on-demand fetch. STOMP subscription can be added for real-time notifications if Waldur confirms proposal events are published.

<details class="page-source"><summary>Page source</summary>

<a href="/source/docs/content/adr/0003-waldur-proposal-workflow.md.txt">Source snapshot</a> · Revision `f928834c8`.

</details>
