---
title: Workplan Queue
---

```js
import {apiFetch, waitForVisible, pollDelay, POLL_HEAVY} from "./components/config.js";
```

```js
const queueState = (async function*() {
  let failures = 0;
  while (true) {
    let stack = [], semantics = {}, ok = false;
    try {
      const [stackResponse, semanticsResponse] = await Promise.all([
        apiFetch("/execution/workplan-stack"),
        apiFetch("/execution/semantics"),
      ]);
      ok = stackResponse.ok && semanticsResponse.ok;
      if (ok) {
        [stack, semantics] = await Promise.all([
          stackResponse.json(),
          semanticsResponse.json(),
        ]);
      }
    } catch {}
    failures = ok ? 0 : failures + 1;
    yield {stack, semantics, ok, ts: new Date()};
    await waitForVisible(pollDelay({ok, base: POLL_HEAVY, failures}));
  }
})();
```

```js
const stack = queueState.stack ?? [];
const semantics = queueState.semantics ?? {};
const _ok = queueState.ok ?? false;
const _ts = queueState.ts;
```

# Workplan Queue

```js
display(html`<div class="queue-live">
  <span style="color:${_ok ? 'var(--theme-foreground-focus)' : 'red'}">●</span>
  ${_ok ? `Live · updated ${_ts?.toLocaleTimeString()}` : html`<span style="color:red">Offline</span>`}
</div>`);
```

```js
const launchModes = Object.keys(semantics.launch_modes ?? {manual: "", queued: "", scheduled: "", immediate: ""});
const concurrencyModes = Object.keys(semantics.concurrency_modes ?? {sequential: "", parallel: ""});

function optionList(values, selected) {
  return values.map(value => html`<option value=${value} selected=${value === selected}>${value}</option>`);
}

function statusCell(row) {
  const classes = ["queue-status", row.eligible ? "eligible" : "blocked"].join(" ");
  return html`<span class=${classes}>${row.eligible ? "eligible" : "blocked"}</span>`;
}

function blockers(row) {
  const parts = [];
  if (row.blocked_by_workstream_ids?.length) parts.push(`${row.blocked_by_workstream_ids.length} workplan`);
  if (row.blocked_by_task_ids?.length) parts.push(`${row.blocked_by_task_ids.length} task`);
  return parts.length ? parts.join(", ") : "—";
}

function queueControls(row) {
  const root = html`<div class="queue-controls"></div>`;
  const mode = html`<select class="queue-select">${optionList(launchModes, row.launch_mode)}</select>`;
  const concurrency = html`<select class="queue-select">${optionList(concurrencyModes, row.concurrency_mode)}</select>`;
  const rank = html`<input class="queue-rank" type="number" min="0" step="1" value=${row.queue_rank ?? ""} aria-label="Queue rank">`;
  const group = html`<input class="queue-group" type="text" value=${row.execution_group ?? ""} aria-label="Execution group">`;
  const message = html`<span class="queue-message"></span>`;
  const save = html`<button class="queue-btn" type="button">Save</button>`;
  const launch = html`<button class="queue-btn queue-btn-primary" type="button">Request</button>`;

  const payload = () => ({
    execution_state: mode.value === "manual" ? "manual" : mode.value === "scheduled" ? "scheduled" : "queued",
    launch_mode: mode.value,
    concurrency_mode: concurrency.value,
    queue_rank: rank.value === "" ? null : Number(rank.value),
    execution_group: group.value.trim() || null,
  });

  async function run(label, action) {
    message.textContent = label;
    message.className = "queue-message";
    save.disabled = true;
    launch.disabled = true;
    try {
      await action();
      message.textContent = "saved";
      message.classList.add("ok");
      setTimeout(() => location.reload(), 450);
    } catch (error) {
      message.textContent = error?.message ?? "failed";
      message.classList.add("error");
      save.disabled = false;
      launch.disabled = false;
    }
  }

  save.onclick = () => run("saving", async () => {
    const response = await apiFetch(`/execution/workplans/${row.workstream_id}/intent`, {
      method: "PATCH",
      headers: {"Content-Type": "application/json"},
      body: JSON.stringify(payload()),
    });
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
  });

  launch.onclick = () => run("requesting", async () => {
    const intent = payload();
    const response = await apiFetch("/execution/launch-requests", {
      method: "POST",
      headers: {"Content-Type": "application/json"},
      body: JSON.stringify({
        workstream_id: row.workstream_id,
        requested_by: "dashboard",
        requested_actor: "activity-core",
        launch_mode: intent.launch_mode,
        concurrency_mode: intent.concurrency_mode,
        immediate_pickup: intent.launch_mode === "immediate",
        priority: row.planning_priority,
        notes: `Queue request from dashboard for ${row.slug}`,
      }),
    });
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
  });

  root.append(mode, concurrency, rank, group, save, launch, message);
  return root;
}
```

```js
if (stack.length === 0) {
  display(html`<p class="queue-empty">No queue candidates.</p>`);
} else {
  display(html`<table class="queue-table">
    <thead>
      <tr>
        <th>State</th>
        <th>Rank</th>
        <th>Workplan</th>
        <th>Lifecycle</th>
        <th>Priority</th>
        <th>Eligibility</th>
        <th>Blocked By</th>
        <th>Intent</th>
      </tr>
    </thead>
    <tbody>${stack.map(row => html`<tr>
      <td>${row.execution_state}</td>
      <td>${row.queue_rank ?? row.planning_order ?? "—"}</td>
      <td><a href=${`./workstreams/${row.workstream_id}`}>${row.slug}</a><div class="queue-title">${row.title}</div></td>
      <td>${row.status}</td>
      <td>${row.planning_priority ?? "—"}</td>
      <td>${statusCell(row)}</td>
      <td>${blockers(row)}</td>
      <td>${queueControls(row)}</td>
    </tr>`)}</tbody>
  </table>`);
}
```

<style>
.queue-live {
  font-size: 0.82rem;
  color: var(--theme-foreground-muted, #666);
  margin: -0.25rem 0 0.75rem;
}
.queue-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.86rem;
}
.queue-table th,
.queue-table td {
  border-bottom: 1px solid var(--theme-foreground-faint, #e5e7eb);
  padding: 0.45rem 0.5rem;
  vertical-align: middle;
  text-align: left;
}
.queue-table th {
  font-size: 0.72rem;
  text-transform: uppercase;
  color: var(--theme-foreground-muted, #666);
}
.queue-title {
  color: var(--theme-foreground-muted, #666);
  font-size: 0.76rem;
  max-width: 28rem;
  overflow-wrap: anywhere;
}
.queue-status {
  display: inline-block;
  min-width: 4.6rem;
  text-align: center;
  border-radius: 6px;
  padding: 0.12rem 0.45rem;
  border: 1px solid var(--theme-foreground-faint, #d1d5db);
  font-size: 0.76rem;
}
.queue-status.eligible {
  color: #166534;
  border-color: #bbf7d0;
  background: #f0fdf4;
}
.queue-status.blocked {
  color: #92400e;
  border-color: #fde68a;
  background: #fffbeb;
}
.queue-controls {
  display: grid;
  grid-template-columns: 7rem 7rem 4.2rem 7rem auto auto minmax(4rem, auto);
  gap: 0.3rem;
  align-items: center;
}
.queue-select,
.queue-rank,
.queue-group {
  height: 1.85rem;
  border: 1px solid var(--theme-foreground-faint, #d1d5db);
  border-radius: 6px;
  background: var(--theme-background, #fff);
  color: var(--theme-foreground, #111);
  font: inherit;
  font-size: 0.78rem;
  padding: 0.1rem 0.35rem;
  min-width: 0;
}
.queue-btn {
  height: 1.85rem;
  border-radius: 6px;
  border: 1px solid var(--theme-foreground-faint, #d1d5db);
  background: var(--theme-background, #fff);
  color: var(--theme-foreground, #111);
  font: inherit;
  font-size: 0.78rem;
  padding: 0 0.55rem;
  cursor: pointer;
}
.queue-btn-primary {
  border-color: #2563eb;
  background: #eff6ff;
  color: #1d4ed8;
}
.queue-message {
  font-size: 0.72rem;
  color: var(--theme-foreground-muted, #666);
  white-space: nowrap;
}
.queue-message.ok {
  color: #16a34a;
}
.queue-message.error {
  color: #dc2626;
}
.queue-empty {
  color: var(--theme-foreground-muted, #666);
}
@media (max-width: 980px) {
  .queue-table,
  .queue-table thead,
  .queue-table tbody,
  .queue-table tr,
  .queue-table th,
  .queue-table td {
    display: block;
  }
  .queue-table thead {
    display: none;
  }
  .queue-table tr {
    border-bottom: 1px solid var(--theme-foreground-faint, #e5e7eb);
    padding: 0.5rem 0;
  }
  .queue-table td {
    border: 0;
    padding: 0.25rem 0;
  }
  .queue-controls {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}
</style>
