Skip to content

feat(platform): workflow human input approvals#821

Merged
larryro merged 9 commits into
mainfrom
feat/workflow-human-input-approvals
Mar 20, 2026
Merged

feat(platform): workflow human input approvals#821
larryro merged 9 commits into
mainfrom
feat/workflow-human-input-approvals

Conversation

@larryro
Copy link
Copy Markdown
Collaborator

@larryro larryro commented Mar 20, 2026

Summary

  • Implement end-to-end human input request flow for workflow executions, allowing LLM agents to pause and request user input via approval cards
  • Split approval status into executingcompleted lifecycle with atomic claim mutations to prevent race conditions
  • Display system messages (rejections, cancellations) as collapsible chat items instead of filtering them out
  • Batch workflow step updates with JSON sanitization for malformed LLM output
  • i18n all approval/human input cards, add ARIA roles and keyboard accessibility
  • Fix human input resume to re-execute the same LLM step (not advance), inject humanInputContext as a template variable
  • Fix Outlook connector pagination with position-based skip for sub-second timestamp precision

Test plan

  • Verify human input request cards render and accept user responses
  • Confirm approval lifecycle transitions: pending → executing → completed/rejected
  • Test that system messages appear as collapsible items in chat
  • Validate batch step updates handle malformed JSON gracefully
  • Check all approval cards display translated strings (no hardcoded text)
  • Verify keyboard navigation and screen reader support on approval cards
  • Run existing test suites: use-merged-chat-items, use-message-processing, execute_approved_workflow_run, on_workflow_complete

Summary by CodeRabbit

  • New Features

    • Workflows now pause to request user input for decisions and clarifications
    • Multi-step workflow updates supported for batch modifications
    • Execution displays "waiting for input" status indicator
  • Improvements

    • Approval display consolidated to show one active approval per session
    • Enhanced approval status lifecycle with new executing and completed states
    • Improved error messaging for approval rejections and failures

Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

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

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 20, 2026

📝 Walkthrough

Walkthrough

This PR refactors the approval system's status model from 'pending' | 'approved' | 'rejected' to 'pending' | 'executing' | 'completed' | 'rejected', replacing direct approval-in-message rendering with a single "active approval" pattern that shows only the latest pending or executing approval at the bottom of the chat. It adds human input request support within workflow steps, including workflow pausing/resuming when waiting for user responses. The PR extends workflow updates to support batch multi-step patches, updates the Outlook connector's cursor-based pagination behavior, and refactors chat components to separate approval rendering via a new ApprovalCardRenderer component.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 19.15% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(platform): workflow human input approvals' directly and concisely summarizes the main change—implementing workflow human input approvals. It clearly indicates the primary objective without unnecessary detail.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/workflow-human-input-approvals
📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

You can customize the tone of the review comments and chat replies.

Configure the tone_instructions setting to customize the tone of the review comments and chat replies. For example, you can set the tone to Act like a strict teacher, Act like a pirate and more.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 17

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (7)
services/platform/convex/workflow_engine/action_defs/workflow_processing_records/workflow_processing_records_action.ts (1)

79-83: ⚠️ Potential issue | 🟡 Minor

Update the example label to match the new status value.

The sample now filters status == "completed", but the bullet still says “Find approved product recommendations”. That keeps the old enum name in the docs and makes the example harder to copy correctly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@services/platform/convex/workflow_engine/action_defs/workflow_processing_records/workflow_processing_records_action.ts`
around lines 79 - 83, Update the example label that precedes the filter for
product approvals in the Examples block of
workflow_processing_records_action.ts: change the bullet text "Find approved
product recommendations" to match the new enum value (e.g., "Find completed
product recommendations") so it aligns with the filterExpression 'status ==
"completed" && resourceType == "product_recommendation" used in the example;
ensure the label and the example remain consistent in the Examples section near
the top of the file.
services/platform/app/features/custom-agents/components/test-chat-panel.tsx (1)

33-60: ⚠️ Potential issue | 🟠 Major

Pass the active approval state into the test composer too.

activeApproval is only wired into TestMessageList. TestChatInput still receives no state that would disable the composer or swap its copy while a human-input/approval card is pending, so the test panel can keep sending normal chat messages during a paused workflow. That makes this surface behave differently from the main chat flow introduced in this PR.

Also applies to: 92-119

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/app/features/custom-agents/components/test-chat-panel.tsx`
around lines 33 - 60, The TestChatInput composer in TestChatPanel isn't
receiving the activeApproval state so it can be disabled or show alternate copy
during pending approvals; update the TestChatPanel JSX where TestChatInput is
rendered to pass the activeApproval prop (from useTestChat) into TestChatInput
(and any related props like isDisabled or approvalState) so the input mirrors
the paused workflow behavior used by TestMessageList; ensure all
instances/usages of TestChatInput in this file (lines near where displayItems...
and the TestMessageList/TestChatInput components are used) receive the
activeApproval prop and the composer uses it to disable input or change copy.
services/platform/convex/agent_tools/integrations/internal_actions.ts (1)

136-169: ⚠️ Potential issue | 🔴 Critical

Add an atomic execution claim before calling the integration.

status === "executing" is only a shared state check. If this action is invoked twice while the approval is still executing, both callers will pass the guard and run executeIntegration(...), which can duplicate non-idempotent writes in the downstream system. Mirror the document-write path and atomically claim/set executedAt before Line 160 so only one executor can cross the side-effect boundary.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/convex/agent_tools/integrations/internal_actions.ts` around
lines 136 - 169, The current guard on approval.status === "executing" is a race;
before calling
internal.agent_tools.integrations.internal_actions.executeIntegration you must
perform an atomic claim/update on the approval document (e.g., in the same
Convex transaction API used elsewhere for document writes) to set an executedAt
timestamp (or flip a claimed flag) only if executedAt is not already set, and
throw/abort if the conditional update fails; update the approval record (using
the same approval id from approval.id / approval._id) and then call
ctx.runAction only after the transaction confirms you successfully
claimed/updated executedAt so only one caller can proceed to executeIntegration.
services/platform/convex/agent_tools/workflows/__tests__/execute_approved_workflow_update.test.ts (1)

173-181: 🧹 Nitpick | 🔵 Trivial

Test name slightly misleading after status model change.

The test is named "throws when approval status is not approved" but now tests rejection of pending status. Consider renaming to "throws when approval status is not executing" to align with the new lifecycle model.

📝 Suggested rename
-  it('throws when approval status is not approved', async () => {
+  it('throws when approval status is not executing', async () => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@services/platform/convex/agent_tools/workflows/__tests__/execute_approved_workflow_update.test.ts`
around lines 173 - 181, Rename the test description so it reflects the new
lifecycle model: change the it(...) string from "throws when approval status is
not approved" to "throws when approval status is not executing" in the test that
calls getHandler(), createMockApproval({ status: 'pending' }), and asserts
handler(ctx, { approvalId: 'approval-1', approvedBy: 'user-1' }) rejects; no
code logic changes required—only update the test name to reference "executing".
services/platform/app/features/chat/components/integration-approval-card.tsx (1)

269-292: ⚠️ Potential issue | 🟡 Minor

Translate the badge label too.

Line 291 still renders the raw status code (executing, completed, etc.), so the approval state is only partially localized even though the surrounding copy now uses translation keys.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/app/features/chat/components/integration-approval-card.tsx`
around lines 269 - 292, Badge currently shows the raw status string (status)
which bypasses translations; change the rendering to use the same translated
label used in the Text block by computing a translated status label (e.g.,
statusLabel via t('statusExecuting') / t('statusCompletedFailed') /
t('statusCompletedSuccess') / t('statusRejected') based on the status and
executionError) and render that variable inside the <Badge> instead of {status};
update the logic near the HStack/Badge in integration-approval-card.tsx
(reference variables: status, executionError, t) so both the caption Text and
the Badge show the localized status.
services/platform/app/features/chat/components/human-input-request-card.tsx (1)

142-163: ⚠️ Potential issue | 🟡 Minor

Disable the card-level option wrappers while submission is in flight.

The inner RadioGroupItem/Checkbox is disabled, but the surrounding button still updates selectedValue / selectedValues. During a pending submit the card can visually switch to a different answer than the one already sent. Mirror isSubmitting on the wrapper click path as well.

Also applies to: 198-209

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/app/features/chat/components/human-input-request-card.tsx`
around lines 142 - 163, The card-level wrapper buttons still call
setSelectedValue / setSelectedValues even when isSubmitting, so the UI can
change while a submit is pending; update the onClick handlers for the option
wrappers (the button elements inside the RadioGroup and the corresponding
checkbox wrapper used around metadata.options and the block around lines
matching the second occurrence) to no-op when isSubmitting is true (i.e., guard
the onClick with if (isSubmitting) return or early exit) so selection only
changes when not submitting, mirroring the disabled state applied to the inner
RadioGroupItem/Checkbox.
services/platform/app/features/chat/components/workflow-creation-approval-card.tsx (1)

552-574: ⚠️ Potential issue | 🟡 Minor

Translate the badge label instead of showing raw status codes.

The caption is localized, but the badge still renders the raw enum (executing, completed, rejected). That leaks backend status codes into non-English UIs and leaves this card partially untranslated.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@services/platform/app/features/chat/components/workflow-creation-approval-card.tsx`
around lines 552 - 574, The badge currently renders the raw status enum (status)
and must show the localized label instead; update the Badge children to use the
translation function (t) like the caption does (e.g., map 'executing' ->
t('statusExecuting'), 'completed' -> t('statusCompletedSuccess' or
'statusCompletedFailed' depending on executionError, and 'rejected' ->
t('statusRejected')) or extract that mapping into a helper (e.g.,
getStatusLabel(status, executionError)) and use it inside the Badge so the UI
shows translated status strings instead of raw backend codes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@examples/integrations/outlook/connector.ts`:
- Around line 386-402: Update the misleading comment to reflect that results are
fetched in descending order by receivedDateTime (orderby: receivedDateTime desc)
and ensure we always trim any over-fetched results down to requestedTop;
specifically, after the cursor handling in the block that references
cursor?.messageId, keep the existing findIndex/slice behavior when the cursor is
found, and add a fallback that sets rawValues = rawValues.slice(0, requestedTop)
when the cursor message is not found (so rawValues never contains more than
requestedTop); refer to the variables cursor, cursor.messageId, rawValues, and
requestedTop when making these changes.

In `@services/platform/app/features/chat/components/chat-messages.tsx`:
- Around line 172-187: The visibility predicate for messages (shouldShow)
currently only checks message.role, message.content, and message.isAborted,
which causes assistant messages that have empty content but non-empty file
payloads to be hidden; update the shouldShow logic used before rendering
MessageBubble to also treat messages with message.attachments.length > 0 or
message.fileParts.length > 0 as visible so assistant messages containing files
reach the MessageBubble component (preserving existing role normalization and
threadId injection).

In
`@services/platform/app/features/chat/components/document-write-approval-card.tsx`:
- Around line 78-83: The current flow calls updateApprovalStatus({ approvalId,
status: 'executing' }) and then separately calls executeDocumentWrite({
approvalId }), which can leave the approval stuck in 'executing' if
executeDocumentWrite fails or the client disconnects; make the transition atomic
by moving the status update into the executeDocumentWrite backend path or by
introducing a single server-side claim-and-execute endpoint (e.g., combine
updateApprovalStatus and execution inside executeDocumentWrite or create
executeAndClaimApproval(approvalId)). Update the client to call only the new
atomic endpoint (remove the separate updateApprovalStatus call) and ensure the
backend performs a transactional claim (set status to executing), runs the
write, and updates final status or reverts on failure so retries/rejects remain
possible.

In
`@services/platform/app/features/chat/components/workflow-run-approval-card.tsx`:
- Around line 287-295: The delayed clear in the onSuccess callback of
workflow-run-approval-card.tsx uses setTimeout without cancellation or
validation, so if a new approval appears within 500ms it will wipe the new
input; fix by capturing the timeout id and cancelling it when the active
approval changes or the component unmounts (use an effect cleanup), or instead
check the current approval identifier (e.g., approval.id / activeApprovalId)
inside the timeout and only call setHumanInputValue / setHumanInputSelected /
setHumanInputMulti if the approval id still matches the one that scheduled the
timeout.

In
`@services/platform/app/features/chat/components/workflow-update-approval-card.tsx`:
- Around line 232-236: The disclosure summary currently outputs hardcoded
English labels ("steps", "Step:", "unknown") when rendering based on
metadata.updateType; update the rendering in the component that uses
metadata.updateType / metadata.stepsConfig / metadata.steps / metadata.stepName
to instead use localized strings from the existing workflowUpdateApproval i18n
object (e.g., workflowUpdateApproval.stepsLabel,
workflowUpdateApproval.stepLabel, workflowUpdateApproval.unknownLabel),
replacing the raw literals so all branches use translated text while preserving
the numeric fallback logic.

In
`@services/platform/app/features/chat/hooks/__tests__/use-merged-chat-items.test.ts`:
- Around line 24-36: The test fixture makeApproval currently sets metadata to {}
as never which forces callers to cast arrays with `as never`; instead give
makeApproval a minimal explicit return type (e.g., an Approval or
Partial<Approval> shape matching what useMergedChatItems expects) and remove the
`as never` coercion, then at each call site use the TypeScript `satisfies`
operator to assert the fixture shape rather than using `as`; update
makeApproval's signature and returned object shape (including metadata and
_creationTime) and replace callers that use `as never` (also the fixtures around
lines 80-84) to use `satisfies` so test data stays type-safe and will break if
useMergedChatItems changes.

In `@services/platform/convex/agent_tools/workflows/internal_mutations.ts`:
- Around line 451-455: The mapping that builds steps is performing a redundant
shallow clone via Object.fromEntries(Object.entries(s.stepUpdates)); update the
steps mapping in the function that constructs the steps array (the lines
building steps: args.steps.map((s) => ({ ... }))) to use s.stepUpdates directly
(i.e., assign stepUpdates: s.stepUpdates) instead of the
Object.fromEntries(Object.entries(...)) no-op clone to simplify and avoid
unnecessary work.
- Around line 28-40: The claimWorkflowApprovalForExecution handler currently
only sets executedAt and misses transitioning status to 'executing'; update the
logic in claimWorkflowApprovalForExecution to atomically check approval.status
=== 'pending' (and that executedAt is unset), then patch the record setting both
executedAt: Date.now() and status: 'executing' in a single ctx.db.patch call (or
return false if status is not 'pending'), ensuring the claim moves the approval
from 'pending' → 'executing' and prevents races.

In `@services/platform/convex/agent_tools/workflows/update_workflow_step_tool.ts`:
- Around line 267-285: Add explicit validation to reject mixed single-step and
batch payloads: after computing isBatch (using Array.isArray(args.steps)), check
if isBatch is true but args.stepRecordId or args.updates is also provided (or
conversely if not isBatch but both steps and single-step fields are present) and
return a failure response with a clear message indicating that mixed payloads
are not allowed; update the validation logic surrounding isBatch, args.steps,
args.stepRecordId, and args.updates in update_workflow_step_tool.ts to fail-fast
when both batch and single-step fields are present.
- Around line 325-353: Move per-entry config validation to after fetching the
step info: for each sanitizedEntries entry, call
ctx.runQuery(internal.wf_step_defs.internal_queries.getStepWithWorkflowInfo, {
stepId: toId<'wfStepDefs'>(entry.stepRecordId) }) and if the lookup fails or
returns null, catch it and push a structured validation error for that entry
(don’t throw). Then compute the resolved step type as entry.updates.stepType ??
info.step.stepType and run validateStepConfig/validateStepUpdates against that
resolved type so config-only edits are validated correctly; keep collecting
warnings/errors into the existing validationErrors/validationWarnings arrays and
return them at the call boundary.

In `@services/platform/convex/approvals/helpers.ts`:
- Around line 231-252: The current listActiveApprovalsByOrganization visits
statuses sequentially and breaks when result reaches limit, causing newer
'executing' approvals to be skipped; change the logic in
listActiveApprovalsByOrganization to first collect approvals from both status
buckets without enforcing a per-status early break (remove the per-status
result.length >= limit breaks), then after the nested loops perform
result.sort((a,b)=>b._creationTime - a._creationTime) and finally slice to the
requested limit (e.g., result = result.slice(0, limit)); keep using the same
ctx.db query withIndex('by_org_status') and the status loop but ensure you only
apply the global limit after merging and sorting.

In `@services/platform/convex/lib/variables/replace_variables_in_string.ts`:
- Around line 37-40: The code preserves unresolved templates in
replaceVariablesInString but replaceVariables currently throws if any markers
remain; choose multi-pass templating: remove the fail-fast throw in
replaceVariables (or replace it with a non-fatal warning/return) so preserved
`{{...}}` tokens from replaceVariablesInString can be handled by downstream
passes; update the logic in the replaceVariables function to allow remaining
template markers instead of throwing, and add a comment explaining multi-pass
behavior referencing replaceVariablesInString and replaceVariables.

In `@services/platform/convex/wf_executions/mutations.ts`:
- Around line 97-119: The code trusts execution.triggerData.approvalId to pick
the thread to notify, which allows caller-supplied triggerData to route
cancellation messages; instead, load the approval via
ctx.db.get(toId<'approvals'>(approvalIdStr)) only as a candidate and verify it
is actually associated with this execution before calling saveMessage (e.g.,
compare approval.executionId or approval.workflowRunId/approval.workflowSlug
against this execution.id or execution.workflowSlug), and skip saveMessage if
the approval does not belong to the current execution; keep using
components.agent and the same message payload but only after the ownership check
to prevent spoofed/stale IDs from writing to unrelated threads.

In
`@services/platform/convex/workflow_engine/action_defs/conversation/conversation_action.ts`:
- Around line 19-26: The file places the const debugLog =
createDebugLog('DEBUG_WORKFLOW', '[Conversations]') between import statements;
move that declaration so all imports come first (i.e., after the import block
containing createConversation and the json validators). Specifically, remove the
inline call between imports and re-add const debugLog = createDebugLog(...)
immediately after the last import (references: createDebugLog, debugLog,
createConversation, jsonRecordValidator, jsonValueValidator) so the file has
only imports up top followed by the debugLog declaration.

In
`@services/platform/convex/workflow_engine/helpers/engine/execute_step_handler.ts`:
- Around line 212-223: The current logic uses pendingApprovals.find(...) which
can pick an older approval if duplicates exist; update the selection to
deterministically choose the newest matching human-input approval by filtering
pendingApprovals for resourceType === 'human_input_request' and stepSlug ===
args.stepSlug, then pick the one with the latest created timestamp (or largest
_id if _id encodes time) and assign its _id to approvalTaskId; alternatively
(preferred) change the tool path that issues request_human_input to return the
created approval ID directly and use that value instead of scanning
pendingApprovals.

In
`@services/platform/convex/workflow_engine/helpers/nodes/llm/utils/build_human_input_context.ts`:
- Around line 29-35: escapeForContext only converts < and >; extend it to also
replace newline characters with the literal "\n" (so the Q/A lines remain
single-line) and escape double quotes (e.g., replace " with &quot; or \" ) so
quoted strings in the prompt cannot break formatting; update escapeForContext
and ensure formatResponse continues to call it (symbols: escapeForContext,
formatResponse, build_human_input_context) so all user questions/responses are
normalized before being inserted into the <human_input_context> block.

In `@services/platform/messages/en.json`:
- Around line 2155-2163: Update the wording under the workflowRunApproval block
so the rejection/cancellation terms match: change the status key value for
"statusRejected" to use the same "Cancelled" wording as "rejectTooltip" ("Cancel
workflow execution") — locate the workflowRunApproval JSON object and update the
"statusRejected" string to "Cancelled" (or the exact cancelled phrasing used by
creation/update approval cards) to keep tooltip and terminal status consistent.

---

Outside diff comments:
In `@services/platform/app/features/chat/components/human-input-request-card.tsx`:
- Around line 142-163: The card-level wrapper buttons still call
setSelectedValue / setSelectedValues even when isSubmitting, so the UI can
change while a submit is pending; update the onClick handlers for the option
wrappers (the button elements inside the RadioGroup and the corresponding
checkbox wrapper used around metadata.options and the block around lines
matching the second occurrence) to no-op when isSubmitting is true (i.e., guard
the onClick with if (isSubmitting) return or early exit) so selection only
changes when not submitting, mirroring the disabled state applied to the inner
RadioGroupItem/Checkbox.

In
`@services/platform/app/features/chat/components/integration-approval-card.tsx`:
- Around line 269-292: Badge currently shows the raw status string (status)
which bypasses translations; change the rendering to use the same translated
label used in the Text block by computing a translated status label (e.g.,
statusLabel via t('statusExecuting') / t('statusCompletedFailed') /
t('statusCompletedSuccess') / t('statusRejected') based on the status and
executionError) and render that variable inside the <Badge> instead of {status};
update the logic near the HStack/Badge in integration-approval-card.tsx
(reference variables: status, executionError, t) so both the caption Text and
the Badge show the localized status.

In
`@services/platform/app/features/chat/components/workflow-creation-approval-card.tsx`:
- Around line 552-574: The badge currently renders the raw status enum (status)
and must show the localized label instead; update the Badge children to use the
translation function (t) like the caption does (e.g., map 'executing' ->
t('statusExecuting'), 'completed' -> t('statusCompletedSuccess' or
'statusCompletedFailed' depending on executionError, and 'rejected' ->
t('statusRejected')) or extract that mapping into a helper (e.g.,
getStatusLabel(status, executionError)) and use it inside the Badge so the UI
shows translated status strings instead of raw backend codes.

In `@services/platform/app/features/custom-agents/components/test-chat-panel.tsx`:
- Around line 33-60: The TestChatInput composer in TestChatPanel isn't receiving
the activeApproval state so it can be disabled or show alternate copy during
pending approvals; update the TestChatPanel JSX where TestChatInput is rendered
to pass the activeApproval prop (from useTestChat) into TestChatInput (and any
related props like isDisabled or approvalState) so the input mirrors the paused
workflow behavior used by TestMessageList; ensure all instances/usages of
TestChatInput in this file (lines near where displayItems... and the
TestMessageList/TestChatInput components are used) receive the activeApproval
prop and the composer uses it to disable input or change copy.

In `@services/platform/convex/agent_tools/integrations/internal_actions.ts`:
- Around line 136-169: The current guard on approval.status === "executing" is a
race; before calling
internal.agent_tools.integrations.internal_actions.executeIntegration you must
perform an atomic claim/update on the approval document (e.g., in the same
Convex transaction API used elsewhere for document writes) to set an executedAt
timestamp (or flip a claimed flag) only if executedAt is not already set, and
throw/abort if the conditional update fails; update the approval record (using
the same approval id from approval.id / approval._id) and then call
ctx.runAction only after the transaction confirms you successfully
claimed/updated executedAt so only one caller can proceed to executeIntegration.

In
`@services/platform/convex/agent_tools/workflows/__tests__/execute_approved_workflow_update.test.ts`:
- Around line 173-181: Rename the test description so it reflects the new
lifecycle model: change the it(...) string from "throws when approval status is
not approved" to "throws when approval status is not executing" in the test that
calls getHandler(), createMockApproval({ status: 'pending' }), and asserts
handler(ctx, { approvalId: 'approval-1', approvedBy: 'user-1' }) rejects; no
code logic changes required—only update the test name to reference "executing".

In
`@services/platform/convex/workflow_engine/action_defs/workflow_processing_records/workflow_processing_records_action.ts`:
- Around line 79-83: Update the example label that precedes the filter for
product approvals in the Examples block of
workflow_processing_records_action.ts: change the bullet text "Find approved
product recommendations" to match the new enum value (e.g., "Find completed
product recommendations") so it aligns with the filterExpression 'status ==
"completed" && resourceType == "product_recommendation" used in the example;
ensure the label and the example remain consistent in the Examples section near
the top of the file.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 51f70001-5d7b-4928-a466-a5b9da4b7930

📥 Commits

Reviewing files that changed from the base of the PR and between 6119803 and 6ecb5ee.

⛔ Files ignored due to path filters (1)
  • services/platform/convex/_generated/api.d.ts is excluded by !**/_generated/**
📒 Files selected for processing (69)
  • .claude/CLAUDE.md
  • examples/integrations/outlook/connector.ts
  • examples/workflows/contract-generation/config.json
  • services/platform/app/features/automations/components/automation-assistant/message-list.test.tsx
  • services/platform/app/features/automations/components/automation-assistant/message-list.tsx
  • services/platform/app/features/automations/executions/executions-table.tsx
  • services/platform/app/features/chat/components/approval-card-renderer.tsx
  • services/platform/app/features/chat/components/chat-input.tsx
  • services/platform/app/features/chat/components/chat-interface.tsx
  • services/platform/app/features/chat/components/chat-messages.tsx
  • services/platform/app/features/chat/components/document-write-approval-card.tsx
  • services/platform/app/features/chat/components/human-input-request-card.tsx
  • services/platform/app/features/chat/components/integration-approval-card.tsx
  • services/platform/app/features/chat/components/workflow-creation-approval-card.tsx
  • services/platform/app/features/chat/components/workflow-run-approval-card.tsx
  • services/platform/app/features/chat/components/workflow-update-approval-card.tsx
  • services/platform/app/features/chat/hooks/__tests__/use-merged-chat-items.test.ts
  • services/platform/app/features/chat/hooks/__tests__/use-message-processing.test.ts
  • services/platform/app/features/chat/hooks/queries.ts
  • services/platform/app/features/chat/hooks/use-execution-status.ts
  • services/platform/app/features/chat/hooks/use-merged-chat-items.ts
  • services/platform/app/features/chat/hooks/use-message-processing.ts
  • services/platform/app/features/custom-agents/components/test-chat-panel.tsx
  • services/platform/app/features/custom-agents/components/test-chat-panel/test-message-list.tsx
  • services/platform/app/features/custom-agents/hooks/use-test-chat.ts
  • services/platform/convex/agent_tools/database/helpers/schema_definitions.ts
  • services/platform/convex/agent_tools/documents/internal_actions.ts
  • services/platform/convex/agent_tools/documents/internal_mutations.ts
  • services/platform/convex/agent_tools/human_input/internal_mutations.ts
  • services/platform/convex/agent_tools/human_input/mutations.ts
  • services/platform/convex/agent_tools/human_input/request_human_input_tool.ts
  • services/platform/convex/agent_tools/integrations/internal_actions.ts
  • services/platform/convex/agent_tools/integrations/internal_mutations.ts
  • services/platform/convex/agent_tools/integrations/verify_approval_tool.ts
  • services/platform/convex/agent_tools/workflows/__tests__/execute_approved_workflow_run.test.ts
  • services/platform/convex/agent_tools/workflows/__tests__/execute_approved_workflow_update.test.ts
  • services/platform/convex/agent_tools/workflows/internal_actions.ts
  • services/platform/convex/agent_tools/workflows/internal_mutations.ts
  • services/platform/convex/agent_tools/workflows/update_workflow_step_tool.ts
  • services/platform/convex/approvals/helpers.ts
  • services/platform/convex/approvals/internal_queries.ts
  • services/platform/convex/approvals/list_approvals_paginated.test.ts
  • services/platform/convex/approvals/mutations.ts
  • services/platform/convex/approvals/queries.ts
  • services/platform/convex/approvals/schema.ts
  • services/platform/convex/approvals/types.ts
  • services/platform/convex/approvals/validators.ts
  • services/platform/convex/conversations/send_message_via_integration.ts
  • services/platform/convex/lib/agent_chat/__tests__/start_agent_chat.test.ts
  • services/platform/convex/lib/agent_chat/start_agent_chat.ts
  • services/platform/convex/lib/create_agent_config.ts
  • services/platform/convex/lib/variables/replace_variables_in_string.ts
  • services/platform/convex/wf_executions/mutations.ts
  • services/platform/convex/wf_executions/queries.ts
  • services/platform/convex/wf_step_defs/internal_mutations.ts
  • services/platform/convex/workflow_engine/action_defs/approval/helpers/types.ts
  • services/platform/convex/workflow_engine/action_defs/conversation/conversation_action.ts
  • services/platform/convex/workflow_engine/action_defs/workflow_processing_records/workflow_processing_records_action.ts
  • services/platform/convex/workflow_engine/helpers/engine/dynamic_workflow_handler.ts
  • services/platform/convex/workflow_engine/helpers/engine/execute_step_handler.ts
  • services/platform/convex/workflow_engine/helpers/engine/on_workflow_complete.test.ts
  • services/platform/convex/workflow_engine/helpers/engine/on_workflow_complete.ts
  • services/platform/convex/workflow_engine/helpers/nodes/llm/execute_agent_with_tools.ts
  • services/platform/convex/workflow_engine/helpers/nodes/llm/execute_llm_node.ts
  • services/platform/convex/workflow_engine/helpers/nodes/llm/utils/build_human_input_context.ts
  • services/platform/convex/workflow_engine/helpers/validation/variables/parse.ts
  • services/platform/convex/workflow_engine/internal_actions.ts
  • services/platform/convex/workflows/executions/update_execution_status.ts
  • services/platform/messages/en.json
💤 Files with no reviewable changes (1)
  • services/platform/convex/lib/agent_chat/tests/start_agent_chat.test.ts

Comment on lines +386 to +402
// Position-based skip: find the cursor message by ID in the asc-ordered
// results and take only messages after it. This avoids timestamp precision
// issues and correctly handles multiple messages within the same second.
// Then trim to the originally requested $top so the caller gets exactly
// the number of messages it asked for.
if (cursor?.messageId) {
const cursorIndex = rawValues.findIndex(function (msg) {
return msg.id === cursor.messageId;
});
if (cursorIndex !== -1) {
rawValues = rawValues.slice(
cursorIndex + 1,
cursorIndex + 1 + requestedTop,
);
}
// If cursor message not found (deleted or outside window), keep all results
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Comment inaccuracy and missing trim when cursor not found.

Two issues:

  1. The comment on line 386 says "asc-ordered results" but the default orderby is receivedDateTime desc (line 314). The logic is correct, but the comment is misleading.

  2. When the cursor message is not found (line 401 fallback), rawValues retains up to requestedTop + 10 messages from the over-fetch. Callers may receive more messages than requested, which could cause unexpected behavior.

Proposed fix
   // Position-based skip: find the cursor message by ID in the
-  // asc-ordered results and take only messages after it. This avoids
+  // results and take only messages after it. This avoids
   // timestamp precision issues and correctly handles multiple messages
   // within the same second. Then trim to the originally requested $top
   // so the caller gets exactly the number of messages it asked for.
   if (cursor?.messageId) {
     const cursorIndex = rawValues.findIndex(function (msg) {
       return msg.id === cursor.messageId;
     });
     if (cursorIndex !== -1) {
       rawValues = rawValues.slice(
         cursorIndex + 1,
         cursorIndex + 1 + requestedTop,
       );
+    } else {
+      // Cursor message not found (deleted or outside window) — trim to
+      // requested count to avoid returning more messages than expected.
+      rawValues = rawValues.slice(0, requestedTop);
     }
-    // If cursor message not found (deleted or outside window), keep all results
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Position-based skip: find the cursor message by ID in the asc-ordered
// results and take only messages after it. This avoids timestamp precision
// issues and correctly handles multiple messages within the same second.
// Then trim to the originally requested $top so the caller gets exactly
// the number of messages it asked for.
if (cursor?.messageId) {
const cursorIndex = rawValues.findIndex(function (msg) {
return msg.id === cursor.messageId;
});
if (cursorIndex !== -1) {
rawValues = rawValues.slice(
cursorIndex + 1,
cursorIndex + 1 + requestedTop,
);
}
// If cursor message not found (deleted or outside window), keep all results
}
// Position-based skip: find the cursor message by ID in the
// results and take only messages after it. This avoids
// timestamp precision issues and correctly handles multiple messages
// within the same second. Then trim to the originally requested $top
// so the caller gets exactly the number of messages it asked for.
if (cursor?.messageId) {
const cursorIndex = rawValues.findIndex(function (msg) {
return msg.id === cursor.messageId;
});
if (cursorIndex !== -1) {
rawValues = rawValues.slice(
cursorIndex + 1,
cursorIndex + 1 + requestedTop,
);
} else {
// Cursor message not found (deleted or outside window) — trim to
// requested count to avoid returning more messages than expected.
rawValues = rawValues.slice(0, requestedTop);
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/integrations/outlook/connector.ts` around lines 386 - 402, Update
the misleading comment to reflect that results are fetched in descending order
by receivedDateTime (orderby: receivedDateTime desc) and ensure we always trim
any over-fetched results down to requestedTop; specifically, after the cursor
handling in the block that references cursor?.messageId, keep the existing
findIndex/slice behavior when the cursor is found, and add a fallback that sets
rawValues = rawValues.slice(0, requestedTop) when the cursor message is not
found (so rawValues never contains more than requestedTop); refer to the
variables cursor, cursor.messageId, rawValues, and requestedTop when making
these changes.

Comment on lines +172 to +187
const shouldShow =
message.role === 'user' ||
message.content !== '' ||
message.isAborted;

return shouldShow ? (
<MessageBubble
key={message.key}
message={{
...message,
role: message.role === 'user' ? 'user' : 'assistant',
threadId: threadId,
}}
onSendFollowUp={onSendFollowUp}
/>
) : null;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't hide assistant messages that only contain files.

shouldShow only checks content and isAborted, so an assistant message with empty text but populated attachments or fileParts now returns null and never reaches MessageBubble. Include file payload presence in the visibility check before filtering it out.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/app/features/chat/components/chat-messages.tsx` around
lines 172 - 187, The visibility predicate for messages (shouldShow) currently
only checks message.role, message.content, and message.isAborted, which causes
assistant messages that have empty content but non-empty file payloads to be
hidden; update the shouldShow logic used before rendering MessageBubble to also
treat messages with message.attachments.length > 0 or message.fileParts.length >
0 as visible so assistant messages containing files reach the MessageBubble
component (preserving existing role normalization and threadId injection).

Comment on lines 78 to 83
await updateApprovalStatus({
approvalId,
status: 'approved',
status: 'executing',
});
await executeDocumentWrite({ approvalId });
} catch (err) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Make the approve transition and execution start atomic.

Line 78 persists executing before Line 82 starts the write in a second request. If that second call fails or the tab disconnects in between, the approval is stuck in executing and the user loses the retry/reject path. Move the status transition into executeDocumentWrite, or add a single claim-and-execute backend entrypoint.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@services/platform/app/features/chat/components/document-write-approval-card.tsx`
around lines 78 - 83, The current flow calls updateApprovalStatus({ approvalId,
status: 'executing' }) and then separately calls executeDocumentWrite({
approvalId }), which can leave the approval stuck in 'executing' if
executeDocumentWrite fails or the client disconnects; make the transition atomic
by moving the status update into the executeDocumentWrite backend path or by
introducing a single server-side claim-and-execute endpoint (e.g., combine
updateApprovalStatus and execution inside executeDocumentWrite or create
executeAndClaimApproval(approvalId)). Update the client to call only the new
atomic endpoint (remove the separate updateApprovalStatus call) and ensure the
backend performs a transactional claim (set status to executing), runs the
write, and updates final status or reverts on failure so retries/rejects remain
possible.

Comment on lines +287 to +295
onSuccess: () => {
// Defer clear so the Convex subscription hides the form first,
// then clear state invisibly for the next human input request
setTimeout(() => {
setHumanInputValue('');
setHumanInputSelected('');
setHumanInputMulti([]);
}, 500);
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't let the previous response timeout wipe the next prompt.

The delayed clear isn't tied to the current approval ID. If the workflow asks a follow-up question within 500 ms, this callback will fire against the new request and clear whatever the user has already typed or selected. Cancel the timeout on approval change/unmount, or only clear when the same approval is still active.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@services/platform/app/features/chat/components/workflow-run-approval-card.tsx`
around lines 287 - 295, The delayed clear in the onSuccess callback of
workflow-run-approval-card.tsx uses setTimeout without cancellation or
validation, so if a new approval appears within 500ms it will wipe the new
input; fix by capturing the timeout id and cancelling it when the active
approval changes or the component unmounts (use an effect cleanup), or instead
check the current approval identifier (e.g., approval.id / activeApprovalId)
inside the timeout and only call setHumanInputValue / setHumanInputSelected /
setHumanInputMulti if the approval id still matches the one that scheduled the
timeout.

Comment on lines 232 to +236
{metadata.updateType === 'full_save'
? `${metadata.stepsConfig?.length ?? 0} steps`
: `Step: ${metadata.stepName ?? 'unknown'}`}
: metadata.updateType === 'multi_step_patch'
? `${metadata.steps?.length ?? 0} steps`
: `Step: ${metadata.stepName ?? 'unknown'}`}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Localize the disclosure summary strings too.

This branch still renders raw English ("steps", Step:, unknown), so the card remains partially untranslated in non-English locales. Move these labels into workflowUpdateApproval like the rest of the component.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@services/platform/app/features/chat/components/workflow-update-approval-card.tsx`
around lines 232 - 236, The disclosure summary currently outputs hardcoded
English labels ("steps", "Step:", "unknown") when rendering based on
metadata.updateType; update the rendering in the component that uses
metadata.updateType / metadata.stepsConfig / metadata.steps / metadata.stepName
to instead use localized strings from the existing workflowUpdateApproval i18n
object (e.g., workflowUpdateApproval.stepsLabel,
workflowUpdateApproval.stepLabel, workflowUpdateApproval.unknownLabel),
replacing the raw literals so all branches use translated text while preserving
the numeric fallback logic.

Comment on lines +97 to +119
// Write system message to thread so the AI knows this was user-initiated
const triggerData = isRecord(execution.triggerData)
? execution.triggerData
: null;
const approvalIdStr = triggerData
? getString(triggerData, 'approvalId')
: undefined;
if (approvalIdStr) {
try {
const approval = await ctx.db.get(toId<'approvals'>(approvalIdStr));
if (approval?.threadId) {
await saveMessage(ctx, components.agent, {
threadId: approval.threadId,
message: {
role: 'system',
content: `[WORKFLOW_CANCELLED]\nWorkflow "${execution.workflowSlug ?? 'unknown'}" was stopped.`,
},
});
}
} catch {
// Non-critical: system message failure should not block cancellation
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't route cancellation messages from triggerData.

Line 101 trusts execution.triggerData.approvalId to choose the destination thread. triggerData is caller-supplied on startWorkflow, so a spoofed or stale approval ID can make cancellation write [WORKFLOW_CANCELLED] into an unrelated approval thread. Resolve the thread from trusted execution data, or verify that the loaded approval actually belongs to this execution/workflow before calling saveMessage.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/convex/wf_executions/mutations.ts` around lines 97 - 119,
The code trusts execution.triggerData.approvalId to pick the thread to notify,
which allows caller-supplied triggerData to route cancellation messages;
instead, load the approval via ctx.db.get(toId<'approvals'>(approvalIdStr)) only
as a candidate and verify it is actually associated with this execution before
calling saveMessage (e.g., compare approval.executionId or
approval.workflowRunId/approval.workflowSlug against this execution.id or
execution.workflowSlug), and skip saveMessage if the approval does not belong to
the current execution; keep using components.agent and the same message payload
but only after the ownership check to prevent spoofed/stale IDs from writing to
unrelated threads.

Comment on lines +19 to 26
import { createDebugLog } from '../../../lib/debug_log';
import {
jsonRecordValidator,
jsonValueValidator,
} from '../../../lib/shared/schemas/utils/json_value';

const debugLog = createDebugLog('DEBUG_WORKFLOW', '[Conversations]');
import { createConversation } from './helpers/create_conversation';
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Import ordering: move const debugLog declaration after all imports.

The debugLog constant declaration appears between imports, which breaks the expected file structure where all imports precede code declarations. Move the createDebugLog call after the last import statement.

♻️ Proposed fix
 import { createDebugLog } from '../../../lib/debug_log';
 import {
   jsonRecordValidator,
   jsonValueValidator,
 } from '../../../lib/shared/schemas/utils/json_value';
-
-const debugLog = createDebugLog('DEBUG_WORKFLOW', '[Conversations]');
 import { createConversation } from './helpers/create_conversation';
 import { createConversationFromEmail } from './helpers/create_conversation_from_email';
 import { createConversationFromSentEmail } from './helpers/create_conversation_from_sent_email';
 ...
 import { updateConversations } from './helpers/update_conversations';
+
+const debugLog = createDebugLog('DEBUG_WORKFLOW', '[Conversations]');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@services/platform/convex/workflow_engine/action_defs/conversation/conversation_action.ts`
around lines 19 - 26, The file places the const debugLog =
createDebugLog('DEBUG_WORKFLOW', '[Conversations]') between import statements;
move that declaration so all imports come first (i.e., after the import block
containing createConversation and the json validators). Specifically, remove the
inline call between imports and re-add const debugLog = createDebugLog(...)
immediately after the last import (references: createDebugLog, debugLog,
createConversation, jsonRecordValidator, jsonValueValidator) so the file has
only imports up top followed by the debugLog declaration.

Comment on lines +212 to +223
const pendingApprovals = await ctx.runQuery(
internal.approvals.internal_queries.listPendingForExecution,
{ executionId: toId<'wfExecutions'>(args.executionId) },
);
const humanInputApproval = pendingApprovals.find(
(a: { resourceType: string; stepSlug?: string }) =>
a.resourceType === 'human_input_request' &&
a.stepSlug === args.stepSlug,
);
if (humanInputApproval) {
approvalTaskId = humanInputApproval._id;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Select the new human-input approval deterministically.

If a retry or duplicate request_human_input call leaves more than one pending approval for this stepSlug, .find() on the execution-wide list can pick an older record. The workflow will then wait on the wrong approval ID. Prefer the newest matching approval here, or better, return the created approval ID directly from the tool path and avoid this scan entirely.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@services/platform/convex/workflow_engine/helpers/engine/execute_step_handler.ts`
around lines 212 - 223, The current logic uses pendingApprovals.find(...) which
can pick an older approval if duplicates exist; update the selection to
deterministically choose the newest matching human-input approval by filtering
pendingApprovals for resourceType === 'human_input_request' and stepSlug ===
args.stepSlug, then pick the one with the latest created timestamp (or largest
_id if _id encodes time) and assign its _id to approvalTaskId; alternatively
(preferred) change the tool path that issues request_human_input to return the
created approval ID directly and use that value instead of scanning
pendingApprovals.

Comment on lines +29 to +35
const escapeForContext = (value: string) =>
value.replace(/</g, '&lt;').replace(/>/g, '&gt;');

const formatResponse = (response: string | string[]) =>
Array.isArray(response)
? response.map(escapeForContext).join(', ')
: escapeForContext(response);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider escaping additional characters for prompt safety.

The escapeForContext function escapes < and > to prevent XML/HTML injection within the <human_input_context> tags. However, user-provided questions/responses may contain:

  • Newlines (\n) that could break the - Q: "..." → A: "..." format
  • Quotes (") that could prematurely close the quoted strings

Consider escaping or replacing these to ensure the context block remains well-formed:

♻️ Suggested enhancement
 const escapeForContext = (value: string) =>
-  value.replace(/</g, '&lt;').replace(/>/g, '&gt;');
+  value
+    .replace(/</g, '&lt;')
+    .replace(/>/g, '&gt;')
+    .replace(/"/g, '&quot;')
+    .replace(/\n/g, ' ');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const escapeForContext = (value: string) =>
value.replace(/</g, '&lt;').replace(/>/g, '&gt;');
const formatResponse = (response: string | string[]) =>
Array.isArray(response)
? response.map(escapeForContext).join(', ')
: escapeForContext(response);
const escapeForContext = (value: string) =>
value
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/\n/g, ' ');
const formatResponse = (response: string | string[]) =>
Array.isArray(response)
? response.map(escapeForContext).join(', ')
: escapeForContext(response);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@services/platform/convex/workflow_engine/helpers/nodes/llm/utils/build_human_input_context.ts`
around lines 29 - 35, escapeForContext only converts < and >; extend it to also
replace newline characters with the literal "\n" (so the Q/A lines remain
single-line) and escape double quotes (e.g., replace " with &quot; or \" ) so
quoted strings in the prompt cannot break formatting; update escapeForContext
and ensure formatResponse continues to call it (symbols: escapeForContext,
formatResponse, build_human_input_context) so all user questions/responses are
normalized before being inserted into the <human_input_context> block.

Comment on lines 2155 to 2163
"workflowRunApproval": {
"approve": "Run workflow",
"reject": "Cancel",
"approveTooltip": "Approve and run this workflow",
"rejectTooltip": "Cancel workflow execution",
"statusApprovedSuccess": "Workflow execution started successfully.",
"statusApprovedFailed": "Workflow execution was approved but failed to start.",
"statusExecuting": "Executing...",
"statusCompleted": "Completed",
"statusCompletedSuccess": "Workflow execution started successfully.",
"statusCompletedFailed": "Workflow execution was approved but failed to start.",
"statusRejected": "Rejected",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use the same cancellation wording for workflow runs.

This block now uses “Cancel workflow execution” in the tooltip, but the terminal status still reads “Rejected”. Once these status messages are shown in chat, that inconsistency becomes visible to users; it should match the “cancelled” wording used by the creation/update approval cards.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/messages/en.json` around lines 2155 - 2163, Update the
wording under the workflowRunApproval block so the rejection/cancellation terms
match: change the status key value for "statusRejected" to use the same
"Cancelled" wording as "rejectTooltip" ("Cancel workflow execution") — locate
the workflowRunApproval JSON object and update the "statusRejected" string to
"Cancelled" (or the exact cancelled phrasing used by creation/update approval
cards) to keep tooltip and terminal status consistent.

larryro added 9 commits March 20, 2026 20:19
…Outlook pagination

Implement end-to-end human input request flow for workflow executions,
allowing LLM agents to pause and request user input via approval cards.
Adds approval queries, mutation handlers, and chat UI components for
reviewing and responding to input requests. Also fixes Outlook connector
cursor-based pagination with position-based skip to handle sub-second
timestamp precision issues.
… input loop

Replace the single 'approved' status with 'executing' and 'completed' to accurately track approval lifecycle — approvals now transition pending→executing→completed/rejected, with the triggering approval auto-updated on workflow completion.

Key fixes:
- Re-execute the same LLM step after human input resume instead of advancing to next step
- Inject humanInputContext as a template variable (not appended to system prompt) so {{humanInputContext}} placeholders in workflow configs resolve correctly
- Write rejection reason as system message to thread so the AI acknowledges user-initiated rejections
- Clear waitingFor with empty string instead of undefined (Convex strips undefined from serialized args)
- Update all approval card UIs for new status values with executing=blue/processing badge
- Add request_human_input tool and {{humanInputContext}} to contract generation workflow
…and LLM input sanitization

Adds batch update support to the workflow step tool with JSON sanitization
for malformed LLM output. Extracts ApprovalCardRenderer component and
refactors chat to show a single active approval at the bottom. Adds human
input context builder for LLM nodes and new approval helper queries.
Show all system messages (approval rejections, workflow cancellations,
etc.) in chat with a compact, expandable UI instead of filtering them
out. Human input response messages continue to render as user bubbles.
…atomic execution claims

Extract hardcoded strings into translation keys across all approval and
human input cards with a shared approvalCommon namespace. Add ARIA roles
and attributes to message lists and interactive form elements. Replace
read-then-check idempotency guards with an atomic claimWorkflowApprovalForExecution
mutation to prevent race conditions. Fix approval queries to only swallow
UnauthorizedError instead of all exceptions. Escape HTML entities in human
input context to prevent LLM injection.
…m message formatting

Remove executedAt early-return checks from approval update mutations since
the atomic claim pattern now prevents double execution. Drop the server-side
concurrent generation guard in startAgentChat to avoid permanently deadlocking
threads when generation crashes. Format system message tags for readability.
…ion claim

Remove unused tCommon import, replace role="checkbox" with aria-pressed
on toggle buttons, and update test mocks to handle claimWorkflowApprovalForExecution.
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