Skip to content

Commit 0bc7538

Browse files
committed
improvement(workflows): lint the manual-run entry check and surface lint findings in apply warnings
1 parent 2fc9739 commit 0bc7538

19 files changed

Lines changed: 402 additions & 59 deletions

File tree

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

Lines changed: 71 additions & 3 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
}
@@ -722,9 +723,12 @@
722723
],
723724
"unresolvedReferences": [],
724725
"tableFieldIssues": [],
726+
"manualRunIssue": null,
725727
"notes": []
726728
},
727-
"warnings": [],
729+
"warnings": [
730+
"Workflow lint found issues. Fix these before continuing: Blocks missing required fields: \"Triage\" (systemPrompt)"
731+
],
728732
"needsRedeployment": true,
729733
"dryRun": false
730734
}
@@ -2655,6 +2659,7 @@
26552659
"fieldIssues": [],
26562660
"unresolvedReferences": [],
26572661
"tableFieldIssues": [],
2662+
"manualRunIssue": null,
26582663
"notes": []
26592664
}
26602665
}
@@ -8813,6 +8818,63 @@
88138818
},
88148819
"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."
88158820
},
8821+
"manualRunIssue": {
8822+
"anyOf": [
8823+
{
8824+
"type": "object",
8825+
"properties": {
8826+
"reason": {
8827+
"type": "string",
8828+
"enum": ["no-trigger", "multiple-api-triggers", "trigger-not-connected"],
8829+
"description": "Why the run is refused."
8830+
},
8831+
"message": {
8832+
"type": "string",
8833+
"description": "The error the manual run raises."
8834+
},
8835+
"blockId": {
8836+
"anyOf": [
8837+
{
8838+
"type": "string"
8839+
},
8840+
{
8841+
"type": "null"
8842+
}
8843+
],
8844+
"description": "Trigger the run would start from, when the problem is that trigger."
8845+
},
8846+
"blockName": {
8847+
"anyOf": [
8848+
{
8849+
"type": "string"
8850+
},
8851+
{
8852+
"type": "null"
8853+
}
8854+
],
8855+
"description": "Display name of that trigger."
8856+
},
8857+
"blockType": {
8858+
"anyOf": [
8859+
{
8860+
"type": "string"
8861+
},
8862+
{
8863+
"type": "null"
8864+
}
8865+
],
8866+
"description": "Registered type of that trigger."
8867+
}
8868+
},
8869+
"required": ["reason", "message", "blockId", "blockName", "blockType"],
8870+
"additionalProperties": false
8871+
},
8872+
{
8873+
"type": "null"
8874+
}
8875+
],
8876+
"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 a manual run can begin. Not raised for a workflow only a chat trigger can start."
8877+
},
88168878
"notes": {
88178879
"type": "array",
88188880
"items": {
@@ -8831,6 +8893,7 @@
88318893
"fieldIssues",
88328894
"unresolvedReferences",
88338895
"tableFieldIssues",
8896+
"manualRunIssue",
88348897
"notes"
88358898
],
88368899
"additionalProperties": false,
@@ -8897,6 +8960,7 @@
88978960
"fieldIssues": [],
88988961
"unresolvedReferences": [],
88998962
"tableFieldIssues": [],
8963+
"manualRunIssue": null,
89008964
"notes": []
89018965
}
89028966
}
@@ -9461,7 +9525,7 @@
94619525
"items": {
94629526
"type": "string"
94639527
},
9464-
"description": "Non-fatal notes about blocks and edges that were normalized or dropped before persistence. Empty when there was nothing to report."
9528+
"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."
94659529
},
94669530
"needsRedeployment": {
94679531
"type": "boolean",
@@ -9581,9 +9645,12 @@
95819645
],
95829646
"unresolvedReferences": [],
95839647
"tableFieldIssues": [],
9648+
"manualRunIssue": null,
95849649
"notes": []
95859650
},
9586-
"warnings": [],
9651+
"warnings": [
9652+
"Workflow lint found issues. Fix these before continuing: Blocks missing required fields: \"Triage\" (systemPrompt)"
9653+
],
95879654
"needsRedeployment": true,
95889655
"dryRun": false
95899656
}
@@ -12554,6 +12621,7 @@
1255412621
"fieldIssues": [],
1255512622
"unresolvedReferences": [],
1255612623
"tableFieldIssues": [],
12624+
"manualRunIssue": null,
1255712625
"notes": []
1255812626
}
1255912627
}

‎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: 5 additions & 1 deletion
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

@@ -419,9 +420,12 @@ const declaredRoutes = [
419420
],
420421
unresolvedReferences: [],
421422
tableFieldIssues: [],
423+
manualRunIssue: null,
422424
notes: [],
423425
},
424-
warnings: [],
426+
warnings: [
427+
'Workflow lint found issues. Fix these before continuing: Blocks missing required fields: "Triage" (systemPrompt)',
428+
],
425429
needsRedeployment: true,
426430
dryRun: false,
427431
},

‎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

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,23 @@ const v2WorkflowLintSchema = z
439439
.describe(
440440
"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."
441441
),
442+
manualRunIssue: z
443+
.object({
444+
reason: z
445+
.enum(['no-trigger', 'multiple-api-triggers', 'trigger-not-connected'])
446+
.describe('Why the run is refused.'),
447+
message: z.string().describe('The error the manual run raises.'),
448+
blockId: z
449+
.string()
450+
.nullable()
451+
.describe('Trigger the run would start from, when the problem is that trigger.'),
452+
blockName: z.string().nullable().describe('Display name of that trigger.'),
453+
blockType: z.string().nullable().describe('Registered type of that trigger.'),
454+
})
455+
.nullable()
456+
.describe(
457+
"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 a manual run can begin. Not raised for a workflow only a chat trigger can start."
458+
),
442459
notes: z.array(z.string()).describe('Advisory notes about the report itself.'),
443460
})
444461
.meta({
@@ -3626,6 +3643,11 @@ export const v2ApplyWorkflowOperationsDataSchema = v2WorkflowGraphWriteResultSch
36263643
'Dry run only: the provisional id the evaluation assigned to each block whose `block_id` was not already a UUID, keyed by the `block_id` you asked for. These are not the ids a committed apply produces — the real apply mints new ones — so never wire a later request against them. Wire edges by `block_id` within one batch, or by the `mintedBlockIds` the real apply returns.'
36273644
),
36283645
lint: v2WorkflowLintSchema,
3646+
warnings: z
3647+
.array(z.string())
3648+
.describe(
3649+
'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.'
3650+
),
36293651
dryRun: z
36303652
.boolean()
36313653
.describe(

‎apps/sim/lib/mothership/agent-cli/engines/lint-scope.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ describe('Mothership workflow lint scope', () => {
9292
fieldIssues: [],
9393
unresolvedReferences: [],
9494
tableFieldIssues: [],
95+
manualRunIssue: null,
9596
notes: [],
9697
})
9798
mocks.secrets.mockResolvedValue({ secrets: [{ envKey: 'WORKFLOW_KEY' }], nextCursorKeys: null })

0 commit comments

Comments
 (0)