Skip to content

feat(platform): workflow schema validation, knowledge scoping, and approval types#826

Merged
larryro merged 5 commits into
mainfrom
feat/workflow-schema-validation-and-knowledge-scoping
Mar 21, 2026
Merged

feat(platform): workflow schema validation, knowledge scoping, and approval types#826
larryro merged 5 commits into
mainfrom
feat/workflow-schema-validation-and-knowledge-scoping

Conversation

@larryro
Copy link
Copy Markdown
Collaborator

@larryro larryro commented Mar 21, 2026

Summary

  • Nested schema validation: Extend workflow input schema to support nested object/array validation with proper type checking at each level
  • Knowledge file scoping: Add knowledgeFileIds config to LLM nodes so rag_search is automatically scoped to specified files
  • Human input tool improvements: Refactor tool args to discriminated union for stricter validation, add "Something else" freetext option to single/multi-select cards, set generation status on response submission
  • Approval types in assistant UI: Introduce collapsible system messages and additional approval card types (workflow run, human input, document write, integration)
  • Example workflow updates: Update contract comparison/generation configs to use file objects (fileId + fileName) instead of plain IDs

Test plan

  • Unit tests for resolve_knowledge_file_ids
  • Unit tests for validate_workflow_input with nested schemas
  • Updated message-list tests for new approval card types
  • Manually test human input cards with "Something else" option in single and multi-select modes
  • Verify knowledge file scoping works end-to-end with rag_search
  • Confirm nested object/array workflow inputs validate correctly at runtime

Summary by CodeRabbit

  • New Features

    • Human input responses display with visual indicators
    • Collapsible system messages with expanded previews
    • Knowledge file scoping for LLM steps to refine AI search results
    • Placeholder text support for input requests
    • Enhanced approval and request state visibility
  • Refactor

    • Improved nested schema validation with clearer error messages
    • Extracted system message rendering for better maintainability
  • Documentation

    • Updated workflow syntax reference with configuration examples

larryro added 2 commits March 21, 2026 13:52
…ool schema

- Add an "Other" freetext option to single_select and multi_select cards
  so users can provide custom responses beyond predefined options
- Refactor tool args to use discriminated union for stricter validation
- Update tool description to guide the agent to always use interactive
  cards instead of plain-text option lists
- Set generationStatus on human input response submission for loading UI
- Add deadline to agent continuation after human input response
- Skip onResponseSubmitted callback in workflow context
… and approval types

Extend workflow input schema to support nested object/array validation with
proper type checking at each level. Add knowledgeFileIds config to LLM nodes
so rag_search is automatically scoped to specified files. Introduce additional
approval types (workflow run, human input, document write, integration) in the
assistant UI with collapsible system messages. Update example workflow configs
to use file objects (fileId + fileName) instead of plain IDs.
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 21, 2026

📝 Walkthrough

Walkthrough

This pull request introduces richer input schema structures and knowledge file scoping throughout the platform. It updates two workflow configurations to use structured objects instead of simple IDs (contract-comparison adds typed file arrays, contract-generation replaces templateFileIds/baseFileId with knowledgeFiles/baseFile objects). On the UI side, it extends the chat assistant to handle additional approval and human input request types, adds a new collapsible system message component, and updates message definitions to support 'system' role messages and human input response markers. The backend gains support for knowledge file ID scoping in LLM nodes through a new knowledgeFileIds configuration field, extends workflow input validation to handle nested object and array schemas, adds placeholder support to the human input tool, and updates workflow documentation with examples of nested schema usage.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.53% 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 accurately captures the primary changes: nested schema validation for workflows, knowledge file scoping for LLM nodes, and expanded approval types for the assistant UI.

✏️ 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-schema-validation-and-knowledge-scoping

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

Tip

CodeRabbit can generate a title for your PR based on the changes.

Add @coderabbitai placeholder anywhere in the title of your PR and CodeRabbit will replace it with a title based on the changes in the PR. You can change the placeholder by changing the reviews.auto_title_placeholder setting.

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: 5

🤖 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/workflows/contract-comparison/config.json`:
- Around line 23-32: The schema for the "files" array documents a minimum of 2
but doesn't enforce it; update the array definition for "files" in config.json
(the object that describes items with properties "fileId" and "fileName") to
include a minItems: 2 constraint so JSON validation will reject inputs with
fewer than two file objects.

In
`@services/platform/app/features/automations/components/automation-assistant/message-list.test.tsx`:
- Around line 34-38: Add explicit unit tests in message-list.test.tsx that
render MessageList (or the tested wrapper) with messages covering the new
branches: create one message object with workflowRunApprovals, another with
humanInputRequests (and a separate test for multiline isHumanInputResponse
content), one with documentWriteApprovals, and one with integrationApprovals;
for each test, pass the message array into the same component setup used in
existing tests and assert the DOM shows the expected approval/selection UI and
that the CollapsibleSystemMessage mock (data-testid="system-message") contains
the correct content for system-message paths. Ensure you use the same prop
shapes/keys used in message-list.tsx (workflowRunApprovals, humanInputRequests,
isHumanInputResponse, documentWriteApprovals, integrationApprovals) so the new
selection/rendering branches are directly exercised and add analogous assertions
to the other case group near the existing approval tests.

In
`@services/platform/app/features/automations/components/automation-assistant/message-list.tsx`:
- Around line 152-156: The regex used to parse human-input responses (inside the
block checking message.isHumanInputResponse && message.role === 'system') fails
on multiline questions because it uses .*? for the question capture; update the
pattern to use [\s\S]*? for the question group so it matches newlines (i.e.,
change the question capture from "(.*?)" to "([\s\S]*?)") and apply the same
change in services/platform/app/features/chat/components/chat-messages.tsx where
the identical pattern is used; ensure the variables match and that response
still falls back to message.content when match is null.

In
`@services/platform/app/features/chat/components/collapsible-system-message.tsx`:
- Around line 20-31: The current split uses string equality to find
lastPreviewIdx causing duplicate content when lines repeat; change nonEmptyLines
and previewLines to preserve original indices (e.g., map lines to objects like
{line, index}), build previewLines by slicing the array of {line,index} pairs
(use nonEmptyWithIndex = lines.map((l, i) => ({line: l, index: i})).filter(p =>
p.line.trim() !== '')), compute lastPreviewIdx from the last preview pair's
index (previewLines[previewLines.length-1].index), and then compute rest by
slicing the original lines starting at lastPreviewIdx + 1; update preview to
join previewLines.map(p => p.line) accordingly so preview and rest split by
source index not string equality.

In
`@services/platform/convex/workflow_engine/helpers/nodes/llm/execute_agent_with_tools.ts`:
- Around line 135-137: The code currently omits knowledgeFileIds when its value
is an empty array because it uses a truthy check; update the merge so it only
omits the field when knowledgeFileIds is undefined (not when it's []), e.g. in
execute_agent_with_tools.ts where _args.knowledgeFileIds is merged, replace the
conditional that tests truthiness with one that tests !== undefined or uses
Object.prototype.hasOwnProperty to preserve explicitly-resolved empty arrays so
an explicit [] remains in the resulting payload.
🪄 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: 35dcc89c-875a-41bf-b056-b4f8a172d175

📥 Commits

Reviewing files that changed from the base of the PR and between f395e98 and cbdbc98.

⛔ Files ignored due to path filters (1)
  • services/platform/convex/_generated/api.d.ts is excluded by !**/_generated/**
📒 Files selected for processing (21)
  • examples/workflows/contract-comparison/config.json
  • examples/workflows/contract-generation/config.json
  • services/platform/app/features/automations/components/automation-assistant.tsx
  • 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/components/automation-assistant/types.ts
  • services/platform/app/features/automations/hooks/use-assistant-chat.ts
  • services/platform/app/features/chat/components/chat-messages.tsx
  • services/platform/app/features/chat/components/collapsible-system-message.tsx
  • services/platform/convex/agent_tools/human_input/request_human_input_tool.ts
  • services/platform/convex/agent_tools/workflows/helpers/syntax_reference.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/types.ts
  • services/platform/convex/workflow_engine/helpers/nodes/llm/utils/__tests__/resolve_knowledge_file_ids.test.ts
  • services/platform/convex/workflow_engine/helpers/nodes/llm/utils/resolve_knowledge_file_ids.ts
  • services/platform/convex/workflow_engine/helpers/nodes/llm/utils/validate_and_normalize_config.ts
  • services/platform/convex/workflow_engine/helpers/validation/steps/start.ts
  • services/platform/convex/workflow_engine/helpers/validation/validate_workflow_input.test.ts
  • services/platform/convex/workflow_engine/helpers/validation/validate_workflow_input.ts
  • services/platform/convex/workflow_engine/types/nodes.ts

Comment on lines +23 to +32
"description": "File objects ordered chronologically (oldest first). Minimum 2 required.",
"items": {
"type": "object",
"properties": {
"fileId": { "type": "string", "description": "Convex storage ID" },
"fileName": { "type": "string", "description": "Original file name" }
},
"required": ["fileId", "fileName"]
}
},
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

files minimum cardinality is documented but not enforced.

The schema says “Minimum 2 required,” but this segment only enforces presence/type, not count. That allows invalid single-file inputs to proceed and break the comparison intent.

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

In `@examples/workflows/contract-comparison/config.json` around lines 23 - 32, The
schema for the "files" array documents a minimum of 2 but doesn't enforce it;
update the array definition for "files" in config.json (the object that
describes items with properties "fileId" and "fileName") to include a minItems:
2 constraint so JSON validation will reject inputs with fewer than two file
objects.

Comment on lines +34 to +38
vi.mock('@/app/features/chat/components/collapsible-system-message', () => ({
CollapsibleSystemMessage: ({ content }: { content: string }) => (
<div data-testid="system-message">{content}</div>
),
}));
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

Add direct coverage for the new approval and system-message branches.

These mocks and props are wired in, but the assertions still only exercise workflow update/creation approvals. Please add direct cases for workflowRunApprovals, humanInputRequests, documentWriteApprovals, integrationApprovals, and a multiline isHumanInputResponse message so the new selection/rendering paths in message-list.tsx are regression-tested.

Also applies to: 96-99

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

In
`@services/platform/app/features/automations/components/automation-assistant/message-list.test.tsx`
around lines 34 - 38, Add explicit unit tests in message-list.test.tsx that
render MessageList (or the tested wrapper) with messages covering the new
branches: create one message object with workflowRunApprovals, another with
humanInputRequests (and a separate test for multiline isHumanInputResponse
content), one with documentWriteApprovals, and one with integrationApprovals;
for each test, pass the message array into the same component setup used in
existing tests and assert the DOM shows the expected approval/selection UI and
that the CollapsibleSystemMessage mock (data-testid="system-message") contains
the correct content for system-message paths. Ensure you use the same prop
shapes/keys used in message-list.tsx (workflowRunApprovals, humanInputRequests,
isHumanInputResponse, documentWriteApprovals, integrationApprovals) so the new
selection/rendering branches are directly exercised and add analogous assertions
to the other case group near the existing approval tests.

Comment on lines +152 to +156
if (message.isHumanInputResponse && message.role === 'system') {
const match = message.content.match(
/^User responded to question "(.*?)": ([\s\S]+)$/,
);
const response = match?.[2] ?? message.content;
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
python - <<'PY'
import re

current = re.compile(r'^User responded to question "(.*?)": ([\s\S]+)$')
fixed = re.compile(r'^User responded to question "([\s\S]*?)": ([\s\S]*)$')

sample = 'User responded to question "Line 1\nLine 2": answer'

for name, pattern in [('current', current), ('fixed', fixed)]:
    match = pattern.match(sample)
    print(name, bool(match), match.group(2) if match else None)
PY

Repository: tale-project/tale

Length of output: 97


🏁 Script executed:

# Check the message-list.tsx file at the specified lines
cat -n services/platform/app/features/automations/components/automation-assistant/message-list.tsx | sed -n '145,165p'

Repository: tale-project/tale

Length of output: 1015


🏁 Script executed:

# Search for the duplicate pattern in chat-messages.tsx
rg "User responded to question" services/platform/app/features/chat/components/

Repository: tale-project/tale

Length of output: 188


🏁 Script executed:

# Get context around the chat-messages.tsx match
rg -B 5 -A 5 "User responded to question" services/platform/app/features/chat/components/chat-messages.tsx

Repository: tale-project/tale

Length of output: 502


🏁 Script executed:

# Search for any other occurrences of this regex pattern in the codebase
rg "User responded to question" services/

Repository: tale-project/tale

Length of output: 936


Fix the regex to handle multiline questions in human-input responses.

The regex pattern /^User responded to question "(.*?)": ([\s\S]+)$/ uses .*? which stops at newline characters. Since multiline text_input prompts are supported, questions containing newlines won't match, causing the fallback to display the entire system message instead of just the user's answer. Change the pattern to use [\s\S]*? for the question capture group at line 154 here and in services/platform/app/features/chat/components/chat-messages.tsx.

Suggested fix
           const match = message.content.match(
-            /^User responded to question "(.*?)": ([\s\S]+)$/,
+            /^User responded to question "([\s\S]*?)": ([\s\S]+)$/,
           );
📝 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
if (message.isHumanInputResponse && message.role === 'system') {
const match = message.content.match(
/^User responded to question "(.*?)": ([\s\S]+)$/,
);
const response = match?.[2] ?? message.content;
if (message.isHumanInputResponse && message.role === 'system') {
const match = message.content.match(
/^User responded to question "([\s\S]*?)": ([\s\S]+)$/,
);
const response = match?.[2] ?? message.content;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@services/platform/app/features/automations/components/automation-assistant/message-list.tsx`
around lines 152 - 156, The regex used to parse human-input responses (inside
the block checking message.isHumanInputResponse && message.role === 'system')
fails on multiline questions because it uses .*? for the question capture;
update the pattern to use [\s\S]*? for the question group so it matches newlines
(i.e., change the question capture from "(.*?)" to "([\s\S]*?)") and apply the
same change in services/platform/app/features/chat/components/chat-messages.tsx
where the identical pattern is used; ensure the variables match and that
response still falls back to message.content when match is null.

Comment on lines +20 to +31
const lines = formatted.split('\n');
const nonEmptyLines = lines.filter((l) => l.trim() !== '');
const previewLines = nonEmptyLines.slice(0, 2);
const preview = previewLines.join(' ');
const lastPreviewIdx =
previewLines.length > 0
? lines.indexOf(previewLines[previewLines.length - 1])
: 0;
const rest = lines
.slice(lastPreviewIdx + 1)
.join('\n')
.trimStart();
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

Split preview and expanded content by source index.

If the first two non-empty lines contain the same text, rest is sliced after the first matching value and the expanded section repeats content that is already in the preview. Build the preview from { line, index } pairs so the split point comes from the original position instead of string equality.

🔧 Suggested fix
   const formatted = content.replace(
     /\[([A-Z][A-Z_]+)\]/g,
     (_, tag: string) => `${tag.replaceAll('_', ' ')} -`,
   );
   const lines = formatted.split('\n');
-  const nonEmptyLines = lines.filter((l) => l.trim() !== '');
-  const previewLines = nonEmptyLines.slice(0, 2);
+  const previewEntries = lines
+    .map((line, index) => ({ line, index }))
+    .filter(({ line }) => line.trim() !== '')
+    .slice(0, 2);
+  const previewLines = previewEntries.map(({ line }) => line);
   const preview = previewLines.join(' ');
   const lastPreviewIdx =
-    previewLines.length > 0
-      ? lines.indexOf(previewLines[previewLines.length - 1])
-      : 0;
+    previewEntries.length > 0
+      ? previewEntries[previewEntries.length - 1].index
+      : -1;
   const rest = lines
     .slice(lastPreviewIdx + 1)
     .join('\n')
     .trimStart();
📝 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 lines = formatted.split('\n');
const nonEmptyLines = lines.filter((l) => l.trim() !== '');
const previewLines = nonEmptyLines.slice(0, 2);
const preview = previewLines.join(' ');
const lastPreviewIdx =
previewLines.length > 0
? lines.indexOf(previewLines[previewLines.length - 1])
: 0;
const rest = lines
.slice(lastPreviewIdx + 1)
.join('\n')
.trimStart();
const lines = formatted.split('\n');
const previewEntries = lines
.map((line, index) => ({ line, index }))
.filter(({ line }) => line.trim() !== '')
.slice(0, 2);
const previewLines = previewEntries.map(({ line }) => line);
const preview = previewLines.join(' ');
const lastPreviewIdx =
previewEntries.length > 0
? previewEntries[previewEntries.length - 1].index
: -1;
const rest = lines
.slice(lastPreviewIdx + 1)
.join('\n')
.trimStart();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@services/platform/app/features/chat/components/collapsible-system-message.tsx`
around lines 20 - 31, The current split uses string equality to find
lastPreviewIdx causing duplicate content when lines repeat; change nonEmptyLines
and previewLines to preserve original indices (e.g., map lines to objects like
{line, index}), build previewLines by slicing the array of {line,index} pairs
(use nonEmptyWithIndex = lines.map((l, i) => ({line: l, index: i})).filter(p =>
p.line.trim() !== '')), compute lastPreviewIdx from the last preview pair's
index (previewLines[previewLines.length-1].index), and then compute rest by
slicing the original lines starting at lastPreviewIdx + 1; update preview to
join previewLines.map(p => p.line) accordingly so preview and rest split by
source index not string equality.

Comment on lines +135 to +137
...(_args.knowledgeFileIds?.length
? { knowledgeFileIds: _args.knowledgeFileIds }
: {}),
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

Do not drop an explicitly empty knowledgeFileIds array.

When resolution yields [], this branch omits the field entirely, which can unintentionally remove scoping and broaden retrieval. Preserve empty arrays if they were explicitly resolved.

🔧 Proposed fix
-    ...(_args.knowledgeFileIds?.length
-      ? { knowledgeFileIds: _args.knowledgeFileIds }
-      : {}),
+    ...(_args.knowledgeFileIds !== undefined
+      ? { knowledgeFileIds: _args.knowledgeFileIds }
+      : {}),
📝 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
...(_args.knowledgeFileIds?.length
? { knowledgeFileIds: _args.knowledgeFileIds }
: {}),
...(_args.knowledgeFileIds !== undefined
? { knowledgeFileIds: _args.knowledgeFileIds }
: {}),
🤖 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/execute_agent_with_tools.ts`
around lines 135 - 137, The code currently omits knowledgeFileIds when its value
is an empty array because it uses a truthy check; update the merge so it only
omits the field when knowledgeFileIds is undefined (not when it's []), e.g. in
execute_agent_with_tools.ts where _args.knowledgeFileIds is merged, replace the
conditional that tests truthiness with one that tests !== undefined or uses
Object.prototype.hasOwnProperty to preserve explicitly-resolved empty arrays so
an explicit [] remains in the resulting payload.

larryro added 2 commits March 21, 2026 14:09
…aiting approval

Workflows waiting for human approval (waitingFor field set) can
legitimately be idle for extended periods. Add a 24-hour extension
to the stuck recovery timeout so these executions aren't incorrectly
marked as failed.
On-demand sync already covers all user-facing scenarios: page view
triggers syncSingleWebsite, table load triggers syncStatuses, and
new website creation schedules a delayed sync. The background cron
is unnecessary.
@larryro larryro merged commit 88c88d7 into main Mar 21, 2026
17 checks passed
@larryro larryro deleted the feat/workflow-schema-validation-and-knowledge-scoping branch March 21, 2026 06:34
yannickmonney pushed a commit that referenced this pull request Apr 8, 2026
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