Skip to content

feat(task-board): run a column's rule when a card lands there, however it moved - #6800

Merged
viktormarinho merged 4 commits into
mainfrom
feat/board-column-automations
Sep 1, 2026
Merged

feat(task-board): run a column's rule when a card lands there, however it moved#6800
viktormarinho merged 4 commits into
mainfrom
feat/board-column-automations

Conversation

@viktormarinho

@viktormarinho viktormarinho commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #6794, whose Settings → Tasks page this builds on. Written against
its branch; retargeted to main after it merged mid-flight.

The gap

The rule lives on the column (task_board_column_automations), but only the
Jira pull ever read it — automationFor had exactly one caller. Dragging a card
into that column in Studio did nothing, which is the gesture the feature is
described by and the one it did not cover.

runColumnAutomation is now the single act. Both the pull and
TASK_BOARD_ITEM_UPDATE go through it, so a column cannot mean one thing to
the sync and another to the board. It fires only on an actual lane change, and
never on a card someone already owns.

A bug the sharing made obvious

The rule was looked up by the card's status and then claimed on the board's
queue lane:

boardAutomationFor(ctx, orgId, item.status)      found on any column
claimUnassignedForSuperAgent(..., lanes.queue)   fenced on one column

A rule on anything but the queue lane found itself, tried to claim where the
card was not, matched nothing, and did nothing. Silently — a lost claim and an
impossible one both come back null.

The claim now fences on the column the rule is on. Worth being precise about
what protects that: the lookup and the fence both read item.status inside one
function, so they cannot diverge. That is structural, not tested. The new
integration test covers the fence's contract — wins on its own column, refuses
one that moved on, exactly one of two concurrent triggers wins, refuses an owned
card — but it would not have caught the original bug, which lived in the caller.

The UI

Settings → Tasks gets "When a card lands in a column": one switch per column
with an optional instruction, beside #6794's system prompt. Neighbours on
purpose, and different things — that prompt is standing context every run
carries; this one is the opening instruction of the run a column fires.

Shown only for a board whose columns are the org's own. On Studio's board the
lifecycle is already decided, and a switch per canonical lane would invite a
team to turn off the thing that makes the board work. Open to being wrong about
that; it is a one-line gate either way.

Off is a deleted row rather than an empty string, so "no rule" has one
representation and automationFor keeps answering null for it.

Verification

Browser, against real Postgres:

  • one switch per column, using the board's own titles
  • toggling persists; the prompt field appears only for a column that is on, and
    saves on blur
  • moving a card into the configured column by hand returns it with
    assigneeId: "super-agent", and the timeline shows status_changed then
    assignee_changed

1056 tests against real Postgres, 0 failures. check, lint, fmt, knip
clean.

Something pre-existing this surfaced

The run does not start on my machine — TierUnavailableError, no model provider
configured locally. Chasing that showed reactToSuperAgentDelegation re-throws
only TaskQuotaError and swallows everything else, so the un-delegate-on-failure
path covers quota alone. Any other dispatch failure leaves the card assigned to
an agent that will never run, which is the state its own comment says it
prevents.

Left alone deliberately: widening what counts as fatal in dispatch is its own
change, not one to make inside a feature PR.


Summary by cubic

Column automation rules now run when a card lands in a configured column, whether the Jira sync moved it or someone dragged it in Studio. The rule fires only on an actual lane change and never on an already-owned card; previously only the sync path read the rule, so a drag did nothing.

Bug Fixes

  • The claim was fenced on the board's queue lane while the rule was looked up by the card's status, so a rule on any other column matched nothing and did nothing, silently; it now fences on the column the rule lives on.
  • Lookup and claim both read item.status inside runColumnAutomation, so they cannot diverge.
  • PR-open advancing used a hardcoded lane-name set that matched nothing on a mirrored board; canAdvance now asks the board's own column order.

New Features

  • Settings → Tasks gains a "Board columns" section: one row per column pairing "Move cards here when …" — each choice naming the actual event Studio detects — with an add-and-delete agent rule and optional instruction, shown only for boards whose columns are the org's own.
  • The todo role is now labeled "Where the agent gives up" — since the fence moved to the column's rule, that is all the role means.

Deliberately left as-is: reactToSuperAgentDelegation only re-throws TaskQuotaError, so any other dispatch failure leaves the card assigned to an agent that will never run. Widening what counts as fatal in dispatch is its own change.

Written for commit 1517f32. Summary will update on new commits.

Review in cubic

…r it moved

The rule lives on the column, but only the Jira pull ever read it. Dragging a
card into that column in Studio did nothing — which is the gesture the feature
was described by ("in column X, run the agent with prompt Y when a card
arrives") and the one it did not cover.

`runColumnAutomation` is now the single act, and both the pull and
`TASK_BOARD_ITEM_UPDATE` go through it, so a column cannot mean one thing to
the sync and another to the board. Fires only on an actual lane change, and
never on a card someone already owns.

This also fixes a bug the sharing made obvious. The rule was looked up by the
card's status and then claimed on the board's QUEUE lane, so a rule on any
other column found itself, tried to claim somewhere the card was not, matched
nothing and did nothing. The claim now fences on the column the rule is on —
and because both the lookup and the fence read `item.status` in one function,
they can no longer diverge. That is structural rather than tested: an
integration test covers the fence's contract, but nothing stops a future
caller passing the wrong lane except there no longer being a lane to pass.

Settings → Tasks gets "When a card lands in a column": one switch per column
with an optional instruction, beside the system prompt from #6794. Shown only
for a board whose columns are the org's own — on Studio's board the lifecycle
is already decided, and a switch per canonical lane would invite a team to turn
off the thing that makes the board work.

Verified in a browser against real Postgres: the switch and prompt persist, and
moving a card into the configured column by hand delegates it to the Super
Agent. The run itself does not start locally (`TierUnavailableError`, no model
provider configured here), which surfaced something pre-existing worth naming:
`reactToSuperAgentDelegation` re-throws only `TaskQuotaError`, so the
un-delegate on failure covers quota alone. Every other dispatch failure leaves
the card assigned to an agent that will never run — the state its comment says
it prevents. Left alone deliberately; widening what is fatal in dispatch is its
own change.
…ther

Two sections listed the same columns of the same board — one asking what a
column means, the other whether the agent runs there. That reads as two
settings when it is two halves of one, and the split is what made the meanings
look like triggers.

Now one "Board columns" section, one row per column: its meaning, and the
switch. The Jira card no longer restates half of it; a board using Studio's
lanes still gets the status mapping there, which is genuinely Jira's business.

Also fixes a label I made wrong earlier today. "Queued for the agent" described
the `todo` role back when the auto-delegate fenced on it. Since the fence moved
to the column the rule is on, that role has exactly one remaining use — where a
card returns when the agent gives up — so the label now says that. A role that
sounds like a trigger is the specific confusion this change exists to remove:
every meaning is a PLACE the lifecycle puts a card, and only the switch starts
anything.

Verified in a browser: one section, five rows for the mirrored board's columns,
the select and the switch each persisting from the same row, and no duplicate
under the Jira integration.
@viktormarinho

Copy link
Copy Markdown
Contributor Author

Merged the two sections after review feedback — they listed the same columns of
the same board, one asking what a column means and the other whether the agent
runs there. Two halves of one question, and the split is what made the meanings
read as triggers.

Now one Board columns section, one row per column:

column means runs the agent
Backlog Nothing special
Em Progresso Being worked on
Code Review Under review
QA Nothing special ● + prompt

The Jira card no longer restates half of it. A board using Studio's lanes still
gets the status mapping there, which is genuinely Jira's business.

Also fixes a label this branch made wrong. "Queued for the agent" described the
todo role while the auto-delegate fenced on it; now that the fence moved to
the column the rule is on, that role has exactly one use left — where a card
returns when the agent gives up — and the label says so. A meaning that sounds
like a trigger is the confusion worth removing: every meaning is a PLACE the
lifecycle puts a card, and only the switch starts anything.

…ngs do

Two things, both about a column being described in Studio's words rather than
the board's.

`ADVANCEABLE` was a hardcoded `{triage, todo, in_progress}` — the lanes a card
may still be pushed into the review phase from. On a mirrored board a card's
column is `Fazendo` or `Code Review`, so the set matched nothing and no card
ever advanced when its PR opened. Same bug as the rest of this arc, missed
because it is a `Set.has` rather than an `===`.

`canAdvance` asks the board instead: is the card's column at or before the
target's, in the board's own order. Every board has ordered columns — a
tracker's are ordered by the tracker — so "has it got past this yet" is
answerable anywhere. A test asserts the canonical board yields exactly the
three lanes the set held, which is what makes the swap a refactor for anyone
not mirroring. A column the board does not have is not advanceable at either
end: a card nobody can place is not one to move.

The settings section now says what happens instead of naming the schema. The
role select was a list of meanings, which read as triggers — "Queued for the
agent" sounds like it queues something. It is now "Move cards here when …"
followed by the EVENT Studio detects, which is the thing actually being chosen:
the agent starts, the agent is done and needs a person, the work shipped, the
agent gave up.

The automation is an add-and-delete rather than a switch, because that is what
the storage does — a row exists or it does not, and deleting is the off switch.
Adding it opens the instruction box under the column with a line saying the
card's title and description already come with it, so the box is the
instruction rather than the whole message.
@viktormarinho

Copy link
Copy Markdown
Contributor Author

Two more, from review.

A PR-open never advanced a card on a mirrored board

ADVANCEABLE was a hardcoded {triage, todo, in_progress} — the lanes a card
may still be pushed into the review phase from. A mirrored board's columns are
Fazendo, Code Review, so the set matched nothing and no card moved when its
PR opened. Same bug as the rest of this arc; I missed it because it is a
Set.has rather than an ===.

canAdvance asks the board: is the card's column at or before the target's, in
the board's own order. Every board has ordered columns, so "has it got past
this yet" is answerable anywhere. A test asserts the canonical board yields
exactly the three lanes the set held — that is what makes the swap a refactor
for anyone not mirroring.

The settings now say what happens

The select was a list of meanings, which read as triggers. "Queued for the
agent" sounds like it queues something; it does not, and after the fence moved
earlier in this branch it only marks where a card returns when the agent gives
up.

It is now the event we detect, which is the thing actually being chosen:

Move cards here whenThe agent starts working on it
The agent is done and needs a person · The work has shipped and settled ·
The agent gave up on it · Never — I move them myself

The automation became add-and-delete rather than a switch, matching what the
storage does — a row exists or it does not. Adding opens the instruction box,
with a line saying the card's title and description already travel with it, so
the box is the instruction rather than the whole message. The bin is the off
switch.

Verified in a browser: adding creates the row, deleting removes it, and the
select persists per column.

"The agent starts working on it" describes a state of the world; the person
choosing it wants to know what we watch for. Each option is now the event
itself:

  A run starts on it, or a pull request opens
  The agent's run finishes
  Its pull requests merged and it sat untouched for five days
  A run fails with no retries left
  Nothing — I move cards here myself

The five-day one is the archive sweep's actual threshold, and the first names
both of its triggers on purpose: a card reaches the in-progress column from a
PR opening, a re-run, and the conflict-resolution fence, so "a pull request is
opened" alone would have been concrete and wrong.

The keys move with the strings — `whenAgentStarts` no longer describes what it
holds.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

10 issues found and verified against the latest diff

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/web/src/views/settings/board-columns.tsx">

<violation number="1" location="apps/web/src/views/settings/board-columns.tsx:88">
P2: When the Tasks settings page renders, `useTaskBoardItems()` subscribes to live board and Decopilot events and processes unrelated task updates. Use the column-only query path so this settings view does not open board subscriptions or run board cache/activity handlers.</violation>

<violation number="2" location="apps/web/src/views/settings/board-columns.tsx:92">
P2: When loading the board fails, this branch tells the user that no columns exist and provides no retry. Handle the query error separately from the legitimate empty-columns case.</violation>

<violation number="3" location="apps/web/src/views/settings/board-columns.tsx:205">
P2: When a user edits an instruction and clicks the trash button, the blur save races the delete and can recreate the automation after deletion. Suppress or cancel the blur save while removing the automation, or serialize these writes.</violation>
</file>

<file name="apps/web/src/hooks/use-column-automations.ts">

<violation number="1" location="apps/web/src/hooks/use-column-automations.ts:31">
P2: When the automation list fails on its initial load, this fallback makes the settings screen treat every configured rule as disabled. Clicking Add can then overwrite an existing rule with the default instruction, so expose the query error and keep the controls unavailable until the list loads successfully.</violation>
</file>

<file name="apps/api/src/tools/task-board/run-column-automation.ts">

<violation number="1" location="apps/api/src/tools/task-board/run-column-automation.ts:61">
P2: When the quota rejects this run, the timeline says the Super Agent owns the card although the catch immediately removes that ownership. Record the assignment only after delegation succeeds, or append a matching rollback activity when the claim is released.</violation>

<violation number="2" location="apps/api/src/tools/task-board/run-column-automation.ts:61">
P1: If the activity insert fails, this function exits after claiming the card and never calls `reactToSuperAgentDelegation`, leaving an assigned card with no run. Make this timeline write best-effort so dispatch still proceeds.</violation>

<violation number="3" location="apps/api/src/tools/task-board/run-column-automation.ts:79">
P1: If someone reassigns the card while delegation is in flight and the delegation then fails, this unconditional update clears that newer assignment and `assignedBy`. Roll back with a conditional claim-release operation that only clears the Super Agent assignment still owned by this automation.</violation>

<violation number="4" location="apps/api/src/tools/task-board/run-column-automation.ts:83">
P2: When a person moves a card into an automated column, other open boards receive the pre-automation item and never learn about the new assignee until refresh. Emit a board update after the conditional claim (and after rollback when applicable), or move the user-path broadcast after automation completes.</violation>
</file>

<file name="apps/api/src/jira/sync.ts">

<violation number="1" location="apps/api/src/jira/sync.ts:334">
P2: When Jira moves an issue between statuses grouped by the same column, this call re-runs the column rule even though the card never landed in a new column. Gate the Jira path on `before?.status !== status` while retaining the create-path behavior.</violation>
</file>

<file name="apps/api/src/tools/task-board/update.ts">

<violation number="1" location="apps/api/src/tools/task-board/update.ts:523">
P2: When the automation lookup, claim, or activity write fails after the status update, this await reports an error after the move is committed and can leave the card unprocessed. Make the trigger retryable or best-effort with claim cleanup, rather than making a one-time status transition the only attempt.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

return await ctx.storage.taskBoard.update(
item.id,
orgId,
{ assigneeId: null, assignedBy: null },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: If someone reassigns the card while delegation is in flight and the delegation then fails, this unconditional update clears that newer assignment and assignedBy. Roll back with a conditional claim-release operation that only clears the Super Agent assignment still owned by this automation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/tools/task-board/run-column-automation.ts, line 79:

<comment>If someone reassigns the card while delegation is in flight and the delegation then fails, this unconditional update clears that newer assignment and `assignedBy`. Roll back with a conditional claim-release operation that only clears the Super Agent assignment still owned by this automation.</comment>

<file context>
@@ -0,0 +1,84 @@
+    return await ctx.storage.taskBoard.update(
+      item.id,
+      orgId,
+      { assigneeId: null, assignedBy: null },
+      by.actor,
+    );
</file context>

);
if (!delegated) return item;

await ctx.storage.taskBoard.recordActivity({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: If the activity insert fails, this function exits after claiming the card and never calls reactToSuperAgentDelegation, leaving an assigned card with no run. Make this timeline write best-effort so dispatch still proceeds.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/tools/task-board/run-column-automation.ts, line 61:

<comment>If the activity insert fails, this function exits after claiming the card and never calls `reactToSuperAgentDelegation`, leaving an assigned card with no run. Make this timeline write best-effort so dispatch still proceeds.</comment>

<file context>
@@ -0,0 +1,84 @@
+  );
+  if (!delegated) return item;
+
+  await ctx.storage.taskBoard.recordActivity({
+    taskBoardItemId: item.id,
+    action: "assignee_changed",
</file context>

const { automations, isPending } = useColumnAutomations();

if (isLoading || isPending) return <Skeleton className="h-40 w-full" />;
if (columns.length === 0) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When loading the board fails, this branch tells the user that no columns exist and provides no retry. Handle the query error separately from the legitimate empty-columns case.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/src/views/settings/board-columns.tsx, line 90:

<comment>When loading the board fails, this branch tells the user that no columns exist and provides no retry. Handle the query error separately from the legitimate empty-columns case.</comment>

<file context>
@@ -0,0 +1,224 @@
+  const { automations, isPending } = useColumnAutomations();
+
+  if (isLoading || isPending) return <Skeleton className="h-40 w-full" />;
+  if (columns.length === 0) {
+    return (
+      <p className="text-xs text-muted-foreground">
</file context>


function BoardColumnRows() {
const t = useT();
const { columns, isLoading } = useTaskBoardItems();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When the Tasks settings page renders, useTaskBoardItems() subscribes to live board and Decopilot events and processes unrelated task updates. Use the column-only query path so this settings view does not open board subscriptions or run board cache/activity handlers.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/src/views/settings/board-columns.tsx, line 86:

<comment>When the Tasks settings page renders, `useTaskBoardItems()` subscribes to live board and Decopilot events and processes unrelated task updates. Use the column-only query path so this settings view does not open board subscriptions or run board cache/activity handlers.</comment>

<file context>
@@ -0,0 +1,224 @@
+
+function BoardColumnRows() {
+  const t = useT();
+  const { columns, isLoading } = useTaskBoardItems();
+  const { automations, isPending } = useColumnAutomations();
+
</file context>

placeholder={t("settings.boardColumns.promptPlaceholder")}
onChange={(e) => setDraft(e.target.value)}
onBlur={() => {
if (draft !== (prompt ?? "")) saveAutomation(draft);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When a user edits an instruction and clicks the trash button, the blur save races the delete and can recreate the automation after deletion. Suppress or cancel the blur save while removing the automation, or serialize these writes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/src/views/settings/board-columns.tsx, line 203:

<comment>When a user edits an instruction and clicks the trash button, the blur save races the delete and can recreate the automation after deletion. Suppress or cancel the blur save while removing the automation, or serialize these writes.</comment>

<file context>
@@ -0,0 +1,224 @@
+            placeholder={t("settings.boardColumns.promptPlaceholder")}
+            onChange={(e) => setDraft(e.target.value)}
+            onBlur={() => {
+              if (draft !== (prompt ?? "")) saveAutomation(draft);
+            }}
+            data-column-automation-prompt={columnKey}
</file context>

return automations;
},
});
return { automations: query.data ?? [], isPending: query.isPending };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When the automation list fails on its initial load, this fallback makes the settings screen treat every configured rule as disabled. Clicking Add can then overwrite an existing rule with the default instruction, so expose the query error and keep the controls unavailable until the list loads successfully.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/src/hooks/use-column-automations.ts, line 31:

<comment>When the automation list fails on its initial load, this fallback makes the settings screen treat every configured rule as disabled. Clicking Add can then overwrite an existing rule with the default instruction, so expose the query error and keep the controls unavailable until the list loads successfully.</comment>

<file context>
@@ -0,0 +1,62 @@
+      return automations;
+    },
+  });
+  return { automations: query.data ?? [], isPending: query.isPending };
+}
+
</file context>

by.actor,
);
}
return delegated;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When a person moves a card into an automated column, other open boards receive the pre-automation item and never learn about the new assignee until refresh. Emit a board update after the conditional claim (and after rollback when applicable), or move the user-path broadcast after automation completes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/tools/task-board/run-column-automation.ts, line 83:

<comment>When a person moves a card into an automated column, other open boards receive the pre-automation item and never learn about the new assignee until refresh. Emit a board update after the conditional claim (and after rollback when applicable), or move the user-path broadcast after automation completes.</comment>

<file context>
@@ -0,0 +1,84 @@
+      by.actor,
+    );
+  }
+  return delegated;
+}
</file context>

);
if (!delegated) return item;

await ctx.storage.taskBoard.recordActivity({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When the quota rejects this run, the timeline says the Super Agent owns the card although the catch immediately removes that ownership. Record the assignment only after delegation succeeds, or append a matching rollback activity when the claim is released.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/tools/task-board/run-column-automation.ts, line 61:

<comment>When the quota rejects this run, the timeline says the Super Agent owns the card although the catch immediately removes that ownership. Record the assignment only after delegation succeeds, or append a matching rollback activity when the claim is released.</comment>

<file context>
@@ -0,0 +1,84 @@
+  );
+  if (!delegated) return item;
+
+  await ctx.storage.taskBoard.recordActivity({
+    taskBoardItemId: item.id,
+    action: "assignee_changed",
</file context>

Comment thread apps/api/src/jira/sync.ts
action: "assignee_changed",
actorId: null,
data: { from: null, to: SUPER_AGENT_ASSIGNEE_ID },
return runColumnAutomation(ctx, item, {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When Jira moves an issue between statuses grouped by the same column, this call re-runs the column rule even though the card never landed in a new column. Gate the Jira path on before?.status !== status while retaining the create-path behavior.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/jira/sync.ts, line 334:

<comment>When Jira moves an issue between statuses grouped by the same column, this call re-runs the column rule even though the card never landed in a new column. Gate the Jira path on `before?.status !== status` while retaining the create-path behavior.</comment>

<file context>
@@ -327,63 +321,20 @@ function mapPriority(issue: JiraIssue): TaskBoardItemPriority {
-    action: "assignee_changed",
-    actorId: null,
-    data: { from: null, to: SUPER_AGENT_ASSIGNEE_ID },
+  return runColumnAutomation(ctx, item, {
+    assignedBy: integration.createdBy,
+    actor: JIRA_SYNC_ACTOR,
</file context>

previous !== null &&
previous.status !== item.status
) {
item = await runColumnAutomation(ctx, item, {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When the automation lookup, claim, or activity write fails after the status update, this await reports an error after the move is committed and can leave the card unprocessed. Make the trigger retryable or best-effort with claim cleanup, rather than making a one-time status transition the only attempt.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/tools/task-board/update.ts, line 523:

<comment>When the automation lookup, claim, or activity write fails after the status update, this await reports an error after the move is committed and can leave the card unprocessed. Make the trigger retryable or best-effort with claim cleanup, rather than making a one-time status transition the only attempt.</comment>

<file context>
@@ -508,6 +509,21 @@ export const TASK_BOARD_ITEM_UPDATE = defineTool({
+      previous !== null &&
+      previous.status !== item.status
+    ) {
+      item = await runColumnAutomation(ctx, item, {
+        assignedBy: getUserId(ctx)!,
+        actor: getUserId(ctx)!,
</file context>

@viktormarinho
viktormarinho merged commit 3699a9d into main Sep 1, 2026
34 checks passed
@viktormarinho
viktormarinho deleted the feat/board-column-automations branch September 1, 2026 17:39
decocms Bot pushed a commit that referenced this pull request Sep 1, 2026
PR: #6800 feat(task-board): run a column's rule when a card lands there, however it moved
Bump type: minor

- decocms (apps/api/package.json): 4.312.1 -> 4.313.0
- @decocms/native (apps/native/package.json): 4.312.1 -> 4.313.0

Deploy-Scope: both
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant