Skip to content

Commit ecd9263

Browse files
authored
improvement(workflows): lint the manual-run entry check and surface lint findings in apply warnings (#9638)
* improvement(workflows): lint the manual-run entry check and surface lint findings in apply warnings * improvement(workflows): describe every case where manualRunIssue is null * docs(workflows): make the apply-operations response example a wired graph
1 parent f0c083c commit ecd9263

19 files changed

Lines changed: 444 additions & 65 deletions

File tree

‎apps/docs/openapi-v2-workflows.json‎

Lines changed: 99 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,7 @@
549549
"fieldIssues": [],
550550
"unresolvedReferences": [],
551551
"tableFieldIssues": [],
552+
"manualRunIssue": null,
552553
"notes": []
553554
}
554555
}
@@ -705,8 +706,20 @@
705706
"agent-1": "a3f1c0b2-7a44-4c1d-9d3a-2b8e5f0a1c77"
706707
},
707708
"lint": {
708-
"sources": [],
709-
"sinks": [],
709+
"sources": [
710+
{
711+
"blockId": "start-1",
712+
"blockName": "Start",
713+
"blockType": "start_trigger"
714+
}
715+
],
716+
"sinks": [
717+
{
718+
"blockId": "a3f1c0b2-7a44-4c1d-9d3a-2b8e5f0a1c77",
719+
"blockName": "Triage",
720+
"blockType": "agent"
721+
}
722+
],
710723
"orphanBlocks": [],
711724
"emptyOutgoingPorts": [],
712725
"invalidBranchPorts": [],
@@ -722,9 +735,12 @@
722735
],
723736
"unresolvedReferences": [],
724737
"tableFieldIssues": [],
738+
"manualRunIssue": null,
725739
"notes": []
726740
},
727-
"warnings": [],
741+
"warnings": [
742+
"Workflow lint found issues. Fix these before continuing: Blocks missing required fields: \"Triage\" (systemPrompt)"
743+
],
728744
"needsRedeployment": true,
729745
"dryRun": false
730746
}
@@ -2655,6 +2671,7 @@
26552671
"fieldIssues": [],
26562672
"unresolvedReferences": [],
26572673
"tableFieldIssues": [],
2674+
"manualRunIssue": null,
26582675
"notes": []
26592676
}
26602677
}
@@ -8813,6 +8830,63 @@
88138830
},
88148831
"description": "Table block `filter` and `order` fields checked against the bound table's live schema that name no column (nor the implicit `id`, `createdAt`, `updatedAt`). Such a run fails inside the block's error edge. A filter holding a `<block.output>` reference, or one that is not JSON, is not checked."
88158832
},
8833+
"manualRunIssue": {
8834+
"anyOf": [
8835+
{
8836+
"type": "object",
8837+
"properties": {
8838+
"reason": {
8839+
"type": "string",
8840+
"enum": ["no-trigger", "multiple-api-triggers", "trigger-not-connected"],
8841+
"description": "Why the run is refused."
8842+
},
8843+
"message": {
8844+
"type": "string",
8845+
"description": "The error the manual run raises."
8846+
},
8847+
"blockId": {
8848+
"anyOf": [
8849+
{
8850+
"type": "string"
8851+
},
8852+
{
8853+
"type": "null"
8854+
}
8855+
],
8856+
"description": "Trigger the run would start from, when the problem is that trigger."
8857+
},
8858+
"blockName": {
8859+
"anyOf": [
8860+
{
8861+
"type": "string"
8862+
},
8863+
{
8864+
"type": "null"
8865+
}
8866+
],
8867+
"description": "Display name of that trigger."
8868+
},
8869+
"blockType": {
8870+
"anyOf": [
8871+
{
8872+
"type": "string"
8873+
},
8874+
{
8875+
"type": "null"
8876+
}
8877+
],
8878+
"description": "Registered type of that trigger."
8879+
}
8880+
},
8881+
"required": ["reason", "message", "blockId", "blockName", "blockType"],
8882+
"additionalProperties": false
8883+
},
8884+
{
8885+
"type": "null"
8886+
}
8887+
],
8888+
"description": "Why a manual run from the editor's Run button would be refused before it starts: no enabled trigger it can start from, more than one API trigger, or a selected trigger with no outgoing connection. `null` when no manual-run issue is reported: the run can begin, only a chat trigger can start the workflow, or the graph is empty (reported in `notes` instead)."
8889+
},
88168890
"notes": {
88178891
"type": "array",
88188892
"items": {
@@ -8831,6 +8905,7 @@
88318905
"fieldIssues",
88328906
"unresolvedReferences",
88338907
"tableFieldIssues",
8908+
"manualRunIssue",
88348909
"notes"
88358910
],
88368911
"additionalProperties": false,
@@ -8897,6 +8972,7 @@
88978972
"fieldIssues": [],
88988973
"unresolvedReferences": [],
88998974
"tableFieldIssues": [],
8975+
"manualRunIssue": null,
89008976
"notes": []
89018977
}
89028978
}
@@ -9461,7 +9537,7 @@
94619537
"items": {
94629538
"type": "string"
94639539
},
9464-
"description": "Non-fatal notes about blocks and edges that were normalized or dropped before persistence. Empty when there was nothing to report."
9540+
"description": "Non-fatal notes about blocks and edges that were normalized or dropped before persistence and, when `lint` has findings, one summary of them, so the findings are not lost to a caller that reads only `warnings`. Empty when there is nothing to report."
94659541
},
94669542
"needsRedeployment": {
94679543
"type": "boolean",
@@ -9564,8 +9640,20 @@
95649640
"agent-1": "a3f1c0b2-7a44-4c1d-9d3a-2b8e5f0a1c77"
95659641
},
95669642
"lint": {
9567-
"sources": [],
9568-
"sinks": [],
9643+
"sources": [
9644+
{
9645+
"blockId": "start-1",
9646+
"blockName": "Start",
9647+
"blockType": "start_trigger"
9648+
}
9649+
],
9650+
"sinks": [
9651+
{
9652+
"blockId": "a3f1c0b2-7a44-4c1d-9d3a-2b8e5f0a1c77",
9653+
"blockName": "Triage",
9654+
"blockType": "agent"
9655+
}
9656+
],
95699657
"orphanBlocks": [],
95709658
"emptyOutgoingPorts": [],
95719659
"invalidBranchPorts": [],
@@ -9581,9 +9669,12 @@
95819669
],
95829670
"unresolvedReferences": [],
95839671
"tableFieldIssues": [],
9672+
"manualRunIssue": null,
95849673
"notes": []
95859674
},
9586-
"warnings": [],
9675+
"warnings": [
9676+
"Workflow lint found issues. Fix these before continuing: Blocks missing required fields: \"Triage\" (systemPrompt)"
9677+
],
95879678
"needsRedeployment": true,
95889679
"dryRun": false
95899680
}
@@ -12554,6 +12645,7 @@
1255412645
"fieldIssues": [],
1255512646
"unresolvedReferences": [],
1255612647
"tableFieldIssues": [],
12648+
"manualRunIssue": null,
1255712649
"notes": []
1255812650
}
1255912651
}

‎apps/sim/app/api/v2/lib/workflow-lint.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,15 @@ export function presentWorkflowLint(lint: WorkflowLintReport) {
6262
field: issue.field,
6363
tableName: issue.tableName,
6464
})),
65+
manualRunIssue: lint.manualRunIssue
66+
? {
67+
reason: lint.manualRunIssue.reason,
68+
message: lint.manualRunIssue.message,
69+
blockId: lint.manualRunIssue.blockId ?? null,
70+
blockName: lint.manualRunIssue.blockName ?? null,
71+
blockType: lint.manualRunIssue.blockType ?? null,
72+
}
73+
: null,
6574
notes: lint.notes,
6675
}
6776
}

‎apps/sim/app/api/v2/workflows/[workflowId]/deploy/route.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ const unquotedRowJson = {
6161
},
6262
],
6363
tableFieldIssues: [],
64+
manualRunIssue: null,
6465
notes: [],
6566
}
6667

‎apps/sim/app/api/v2/workflows/[workflowId]/operations/route.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ const LINT = {
5252
fieldIssues: [],
5353
unresolvedReferences: [],
5454
tableFieldIssues: [],
55+
manualRunIssue: null,
5556
notes: [],
5657
}
5758

‎apps/sim/app/api/v2/workflows/[workflowId]/state/route.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ const EMPTY_LINT = {
7373
fieldIssues: [],
7474
unresolvedReferences: [],
7575
tableFieldIssues: [],
76+
manualRunIssue: null,
7677
notes: [],
7778
}
7879

‎apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts‎

Lines changed: 10 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,13 @@ import type {
2929
StreamToolData,
3030
} from '@/lib/workflows/executor/execution-events'
3131
import { collectInputFormatFiles, isFileFieldType } from '@/lib/workflows/input-format'
32+
import { resolveManualRunEntry } from '@/lib/workflows/triggers/manual-run'
3233
import {
3334
resolveStartCandidates,
3435
StartBlockPath,
3536
TriggerUtils,
3637
} from '@/lib/workflows/triggers/triggers'
37-
import {
38-
extractTriggerMockPayload,
39-
selectBestTrigger,
40-
triggerNeedsMockPayload,
41-
} from '@/lib/workflows/triggers/utils'
38+
import { extractTriggerMockPayload, triggerNeedsMockPayload } from '@/lib/workflows/triggers/utils'
4239
import { useCurrentWorkflow } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-current-workflow'
4340
import { getRunFromBlockDependencyState } from '@/app/workspace/[workspaceId]/w/[workflowId]/utils/run-from-block'
4441
import {
@@ -1096,63 +1093,26 @@ export function useWorkflowExecution() {
10961093

10971094
startBlockId = startBlock.blockId
10981095
} else {
1099-
const candidates = resolveStartCandidates(enabledStates, {
1100-
execution: 'manual',
1101-
})
1102-
1103-
if (candidates.length === 0) {
1104-
const error = new WorkflowValidationError(
1105-
'Workflow requires at least one trigger block to execute',
1106-
'validation',
1107-
'validation',
1108-
'Workflow Validation'
1109-
)
1110-
logger.error('No trigger blocks found for manual run', {
1096+
const entry = resolveManualRunEntry(enabledStates, workflowEdges)
1097+
if (!entry.ok) {
1098+
logger.error('Manual run has no valid entry', {
1099+
reason: entry.issue.reason,
1100+
startBlockId: entry.issue.blockId,
11111101
allBlockTypes: Object.values(enabledStates).map((b) => b.type),
11121102
})
11131103
if (activeWorkflowId) finishOwnedExecution(activeWorkflowId, persistenceExecution)
1114-
throw error
1115-
}
1116-
1117-
// Check for multiple API triggers (still not allowed)
1118-
const apiCandidates = candidates.filter(
1119-
(candidate) => candidate.path === StartBlockPath.SPLIT_API
1120-
)
1121-
if (apiCandidates.length > 1) {
1122-
const error = new WorkflowValidationError(
1123-
'Multiple API Trigger blocks found. Keep only one.',
1104+
throw new WorkflowValidationError(
1105+
entry.issue.message,
11241106
'validation',
11251107
'validation',
11261108
'Workflow Validation'
11271109
)
1128-
logger.error('Multiple API triggers found')
1129-
if (activeWorkflowId) finishOwnedExecution(activeWorkflowId, persistenceExecution)
1130-
throw error
11311110
}
11321111

1133-
// Priority: Start Block > Schedules > External Triggers > Legacy
1134-
const selectedTriggers = selectBestTrigger(candidates, workflowEdges)
1135-
1136-
const selectedCandidate = selectedTriggers[0]
1112+
const selectedCandidate = entry.candidate
11371113
startBlockId = selectedCandidate.blockId
11381114
const selectedTrigger = selectedCandidate.block
11391115

1140-
if (selectedCandidate.path !== StartBlockPath.LEGACY_STARTER) {
1141-
const outgoingConnections = workflowEdges.filter((edge) => edge.source === startBlockId)
1142-
if (outgoingConnections.length === 0) {
1143-
const triggerName = selectedTrigger.name || selectedTrigger.type
1144-
const error = new WorkflowValidationError(
1145-
`${triggerName} must be connected to other blocks to execute`,
1146-
'validation',
1147-
'validation',
1148-
'Workflow Validation'
1149-
)
1150-
logger.error('Trigger has no outgoing connections', { triggerName, startBlockId })
1151-
if (activeWorkflowId) finishOwnedExecution(activeWorkflowId, persistenceExecution)
1152-
throw error
1153-
}
1154-
}
1155-
11561116
if (triggerNeedsMockPayload(selectedCandidate)) {
11571117
const mockPayload = extractTriggerMockPayload(selectedCandidate)
11581118
finalWorkflowInput = mockPayload

‎apps/sim/lib/api/contracts/v2/openapi/workflows.ts‎

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ const EMPTY_LINT_EXAMPLE = {
116116
fieldIssues: [],
117117
unresolvedReferences: [],
118118
tableFieldIssues: [],
119+
manualRunIssue: null,
119120
notes: [],
120121
} as const
121122

@@ -402,8 +403,20 @@ const declaredRoutes = [
402403
inputValidationErrors: [],
403404
mintedBlockIds: { 'agent-1': 'a3f1c0b2-7a44-4c1d-9d3a-2b8e5f0a1c77' },
404405
lint: {
405-
sources: [],
406-
sinks: [],
406+
sources: [
407+
{
408+
blockId: 'start-1',
409+
blockName: 'Start',
410+
blockType: 'start_trigger',
411+
},
412+
],
413+
sinks: [
414+
{
415+
blockId: 'a3f1c0b2-7a44-4c1d-9d3a-2b8e5f0a1c77',
416+
blockName: 'Triage',
417+
blockType: 'agent',
418+
},
419+
],
407420
orphanBlocks: [],
408421
emptyOutgoingPorts: [],
409422
invalidBranchPorts: [],
@@ -419,9 +432,12 @@ const declaredRoutes = [
419432
],
420433
unresolvedReferences: [],
421434
tableFieldIssues: [],
435+
manualRunIssue: null,
422436
notes: [],
423437
},
424-
warnings: [],
438+
warnings: [
439+
'Workflow lint found issues. Fix these before continuing: Blocks missing required fields: "Triage" (systemPrompt)',
440+
],
425441
needsRedeployment: true,
426442
dryRun: false,
427443
},

‎apps/sim/lib/api/contracts/v2/workflow-graph.test.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ const EMPTY_LINT = {
103103
fieldIssues: [],
104104
unresolvedReferences: [],
105105
tableFieldIssues: [],
106+
manualRunIssue: null,
106107
notes: [],
107108
}
108109

@@ -174,6 +175,13 @@ const FULL_LINT = {
174175
tableName: 'Leads',
175176
},
176177
],
178+
manualRunIssue: {
179+
reason: 'trigger-not-connected',
180+
message: 'Start must be connected to other blocks to execute',
181+
blockId: 'block-1',
182+
blockName: 'Start',
183+
blockType: 'start_trigger',
184+
},
177185
notes: ['lint note'],
178186
}
179187

0 commit comments

Comments
 (0)