From 9dcf92bd1434a48e43ed1d3e73937955c4f661ec Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 5 Feb 2026 15:31:15 -0800 Subject: [PATCH 1/4] fix(executor): loop sentinel-end wrongly queued (#3148) * fix(executor): loop sentinel-end wrongly queued * fix nested subflow error highlighting --- .../hooks/use-workflow-execution.ts | 7 ++ .../preview-workflow/preview-workflow.tsx | 10 ++- .../executor/execution/edge-manager.test.ts | 81 +++++++++++++++++++ apps/sim/executor/execution/edge-manager.ts | 8 +- 4 files changed, 104 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts index 0b4916a2f..6b1f8914e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts @@ -491,6 +491,13 @@ export function useWorkflowExecution() { updateActiveBlocks(data.blockId, false) setBlockRunStatus(data.blockId, 'error') + executedBlockIds.add(data.blockId) + accumulatedBlockStates.set(data.blockId, { + output: { error: data.error }, + executed: true, + executionTime: data.durationMs || 0, + }) + accumulatedBlockLogs.push( createBlockLogEntry(data, { success: false, output: {}, error: data.error }) ) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow/preview-workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow/preview-workflow.tsx index 0e6948ac4..cdad58544 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow/preview-workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow/preview-workflow.tsx @@ -349,7 +349,15 @@ export function PreviewWorkflow({ if (block.type === 'loop' || block.type === 'parallel') { const isSelected = selectedBlockId === blockId const dimensions = calculateContainerDimensions(blockId, workflowState.blocks) - const subflowExecutionStatus = getSubflowExecutionStatus(blockId) + + // Check for direct error on the subflow block itself (e.g., loop resolution errors) + // before falling back to children-derived status + const directExecution = blockExecutionMap.get(blockId) + const subflowExecutionStatus: ExecutionStatus | undefined = + directExecution?.status === 'error' + ? 'error' + : (getSubflowExecutionStatus(blockId) ?? + (directExecution ? (directExecution.status as ExecutionStatus) : undefined)) nodeArray.push({ id: blockId, diff --git a/apps/sim/executor/execution/edge-manager.test.ts b/apps/sim/executor/execution/edge-manager.test.ts index 680e890eb..ffc48bc5f 100644 --- a/apps/sim/executor/execution/edge-manager.test.ts +++ b/apps/sim/executor/execution/edge-manager.test.ts @@ -2478,6 +2478,9 @@ describe('EdgeManager', () => { expect(readyNodes).toContain(otherBranchId) expect(readyNodes).not.toContain(sentinelStartId) + // sentinel_end should NOT be ready - it's on a fully deactivated path + expect(readyNodes).not.toContain(sentinelEndId) + // afterLoop should NOT be ready - its incoming edge from sentinel_end should be deactivated expect(readyNodes).not.toContain(afterLoopId) @@ -2545,6 +2548,84 @@ describe('EdgeManager', () => { expect(edgeManager.isNodeReady(afterParallelNode)).toBe(true) }) + it('should not queue loop sentinel-end when upstream condition deactivates entire loop branch', () => { + // Regression test for: upstream condition → (if) → ... many blocks ... → sentinel_start → body → sentinel_end + // → (else) → exit_block + // When condition takes "else", the deep cascade deactivation should NOT queue sentinel_end. + // Previously, sentinel_end was flagged as a cascadeTarget (terminal control node) and + // spuriously queued, causing it to attempt loop scope initialization and fail. + + const conditionId = 'condition' + const intermediateId = 'intermediate' + const sentinelStartId = 'sentinel-start' + const loopBodyId = 'loop-body' + const sentinelEndId = 'sentinel-end' + const afterLoopId = 'after-loop' + const exitBlockId = 'exit-block' + + const conditionNode = createMockNode(conditionId, [ + { target: intermediateId, sourceHandle: 'condition-if' }, + { target: exitBlockId, sourceHandle: 'condition-else' }, + ]) + + const intermediateNode = createMockNode( + intermediateId, + [{ target: sentinelStartId }], + [conditionId] + ) + + const sentinelStartNode = createMockNode( + sentinelStartId, + [{ target: loopBodyId }], + [intermediateId] + ) + + const loopBodyNode = createMockNode( + loopBodyId, + [{ target: sentinelEndId }], + [sentinelStartId] + ) + + const sentinelEndNode = createMockNode( + sentinelEndId, + [ + { target: sentinelStartId, sourceHandle: 'loop_continue' }, + { target: afterLoopId, sourceHandle: 'loop_exit' }, + ], + [loopBodyId] + ) + + const afterLoopNode = createMockNode(afterLoopId, [], [sentinelEndId]) + const exitBlockNode = createMockNode(exitBlockId, [], [conditionId]) + + const nodes = new Map([ + [conditionId, conditionNode], + [intermediateId, intermediateNode], + [sentinelStartId, sentinelStartNode], + [loopBodyId, loopBodyNode], + [sentinelEndId, sentinelEndNode], + [afterLoopId, afterLoopNode], + [exitBlockId, exitBlockNode], + ]) + + const dag = createMockDAG(nodes) + const edgeManager = new EdgeManager(dag) + + const readyNodes = edgeManager.processOutgoingEdges(conditionNode, { + selectedOption: 'else', + }) + + // Only exitBlock should be ready + expect(readyNodes).toContain(exitBlockId) + + // Nothing on the deactivated path should be queued + expect(readyNodes).not.toContain(intermediateId) + expect(readyNodes).not.toContain(sentinelStartId) + expect(readyNodes).not.toContain(loopBodyId) + expect(readyNodes).not.toContain(sentinelEndId) + expect(readyNodes).not.toContain(afterLoopId) + }) + it('should still correctly handle normal loop exit (not deactivate when loop runs)', () => { // When a loop actually executes and exits normally, after_loop should become ready const sentinelStartId = 'sentinel-start' diff --git a/apps/sim/executor/execution/edge-manager.ts b/apps/sim/executor/execution/edge-manager.ts index 68a936104..d2b8a0595 100644 --- a/apps/sim/executor/execution/edge-manager.ts +++ b/apps/sim/executor/execution/edge-manager.ts @@ -71,7 +71,13 @@ export class EdgeManager { for (const targetId of cascadeTargets) { if (!readyNodes.includes(targetId) && !activatedTargets.includes(targetId)) { - if (this.isTargetReady(targetId)) { + // Only queue cascade terminal control nodes when ALL outgoing edges from the + // current node were deactivated (dead-end scenario). When some edges are + // activated, terminal control nodes on deactivated branches should NOT be + // queued - they will be reached through the normal activated path's completion. + // This prevents loop/parallel sentinels on fully deactivated paths (e.g., an + // upstream condition took a different branch) from being spuriously executed. + if (activatedTargets.length === 0 && this.isTargetReady(targetId)) { readyNodes.push(targetId) } } From c0b22a64902ec1d03a38ad23c362b42d5af671c7 Mon Sep 17 00:00:00 2001 From: Waleed Date: Thu, 5 Feb 2026 18:44:24 -0800 Subject: [PATCH 2/4] fix(linear): align tool outputs, queries, and pagination with API (#3150) * fix(linear): align tool outputs, queries, and pagination with API * fix(linear): coerce first param to number, remove duplicate conditions, add null guard --- apps/docs/content/docs/en/tools/linear.mdx | 219 ++++++++++++++++-- apps/sim/blocks/blocks/linear.ts | 130 ++++++++--- apps/sim/tools/linear/create_customer.ts | 4 + .../tools/linear/create_customer_status.ts | 21 +- apps/sim/tools/linear/create_cycle.ts | 12 +- apps/sim/tools/linear/create_label.ts | 4 + apps/sim/tools/linear/create_project_label.ts | 3 + .../tools/linear/create_project_milestone.ts | 16 +- .../sim/tools/linear/create_project_status.ts | 4 + .../sim/tools/linear/create_workflow_state.ts | 14 +- apps/sim/tools/linear/get_active_cycle.ts | 12 +- apps/sim/tools/linear/get_customer.ts | 4 + apps/sim/tools/linear/get_cycle.ts | 1 + .../tools/linear/list_customer_requests.ts | 2 +- .../tools/linear/list_customer_statuses.ts | 42 +++- apps/sim/tools/linear/list_customer_tiers.ts | 39 +++- apps/sim/tools/linear/list_customers.ts | 6 +- apps/sim/tools/linear/list_cycles.ts | 1 + apps/sim/tools/linear/list_labels.ts | 4 + apps/sim/tools/linear/list_project_labels.ts | 60 ++++- .../tools/linear/list_project_milestones.ts | 47 +++- .../sim/tools/linear/list_project_statuses.ts | 44 +++- apps/sim/tools/linear/list_projects.ts | 2 +- apps/sim/tools/linear/list_workflow_states.ts | 4 + apps/sim/tools/linear/search_issues.ts | 13 +- apps/sim/tools/linear/types.ts | 87 ++++++- apps/sim/tools/linear/update_attachment.ts | 1 + apps/sim/tools/linear/update_customer.ts | 4 + .../tools/linear/update_customer_status.ts | 23 +- apps/sim/tools/linear/update_label.ts | 4 + apps/sim/tools/linear/update_project_label.ts | 3 + .../tools/linear/update_project_milestone.ts | 26 ++- .../sim/tools/linear/update_project_status.ts | 4 + .../sim/tools/linear/update_workflow_state.ts | 13 +- 34 files changed, 721 insertions(+), 152 deletions(-) diff --git a/apps/docs/content/docs/en/tools/linear.mdx b/apps/docs/content/docs/en/tools/linear.mdx index b64d9a915..7d472afce 100644 --- a/apps/docs/content/docs/en/tools/linear.mdx +++ b/apps/docs/content/docs/en/tools/linear.mdx @@ -320,6 +320,7 @@ Search for issues in Linear using full-text search | `teamId` | string | No | Filter by team ID | | `includeArchived` | boolean | No | Include archived issues in search results | | `first` | number | No | Number of results to return \(default: 50\) | +| `after` | string | No | Cursor for pagination | #### Output @@ -754,6 +755,10 @@ List all labels in Linear workspace or team | ↳ `name` | string | Label name | | ↳ `color` | string | Label color \(hex\) | | ↳ `description` | string | Label description | +| ↳ `isGroup` | boolean | Whether this label is a group | +| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | +| ↳ `updatedAt` | string | Last update timestamp \(ISO 8601\) | +| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) | | ↳ `team` | object | Team object | | ↳ `id` | string | Team ID | | ↳ `name` | string | Team name | @@ -780,6 +785,10 @@ Create a new label in Linear | ↳ `name` | string | Label name | | ↳ `color` | string | Label color \(hex\) | | ↳ `description` | string | Label description | +| ↳ `isGroup` | boolean | Whether this label is a group | +| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | +| ↳ `updatedAt` | string | Last update timestamp \(ISO 8601\) | +| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) | | ↳ `team` | object | Team object | | ↳ `id` | string | Team ID | | ↳ `name` | string | Team name | @@ -806,6 +815,10 @@ Update an existing label in Linear | ↳ `name` | string | Label name | | ↳ `color` | string | Label color \(hex\) | | ↳ `description` | string | Label description | +| ↳ `isGroup` | boolean | Whether this label is a group | +| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | +| ↳ `updatedAt` | string | Last update timestamp \(ISO 8601\) | +| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) | | ↳ `team` | object | Team object | | ↳ `id` | string | Team ID | | ↳ `name` | string | Team name | @@ -849,9 +862,13 @@ List all workflow states (statuses) in Linear | `states` | array | Array of workflow states | | ↳ `id` | string | State ID | | ↳ `name` | string | State name \(e.g., "Todo", "In Progress"\) | -| ↳ `type` | string | State type \(unstarted, started, completed, canceled\) | +| ↳ `description` | string | State description | +| ↳ `type` | string | State type \(triage, backlog, unstarted, started, completed, canceled\) | | ↳ `color` | string | State color \(hex\) | | ↳ `position` | number | State position in workflow | +| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | +| ↳ `updatedAt` | string | Last update timestamp \(ISO 8601\) | +| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) | | ↳ `team` | object | Team object | | ↳ `id` | string | Team ID | | ↳ `name` | string | Team name | @@ -877,11 +894,17 @@ Create a new workflow state (status) in Linear | --------- | ---- | ----------- | | `state` | object | The created workflow state | | ↳ `id` | string | State ID | -| ↳ `name` | string | State name | -| ↳ `type` | string | State type | -| ↳ `color` | string | State color | -| ↳ `position` | number | State position | -| ↳ `team` | object | Team this state belongs to | +| ↳ `name` | string | State name \(e.g., "Todo", "In Progress"\) | +| ↳ `description` | string | State description | +| ↳ `type` | string | State type \(triage, backlog, unstarted, started, completed, canceled\) | +| ↳ `color` | string | State color \(hex\) | +| ↳ `position` | number | State position in workflow | +| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | +| ↳ `updatedAt` | string | Last update timestamp \(ISO 8601\) | +| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) | +| ↳ `team` | object | Team object | +| ↳ `id` | string | Team ID | +| ↳ `name` | string | Team name | ### `linear_update_workflow_state` @@ -903,10 +926,17 @@ Update an existing workflow state in Linear | --------- | ---- | ----------- | | `state` | object | The updated workflow state | | ↳ `id` | string | State ID | -| ↳ `name` | string | State name | -| ↳ `type` | string | State type | -| ↳ `color` | string | State color | -| ↳ `position` | number | State position | +| ↳ `name` | string | State name \(e.g., "Todo", "In Progress"\) | +| ↳ `description` | string | State description | +| ↳ `type` | string | State type \(triage, backlog, unstarted, started, completed, canceled\) | +| ↳ `color` | string | State color \(hex\) | +| ↳ `position` | number | State position in workflow | +| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | +| ↳ `updatedAt` | string | Last update timestamp \(ISO 8601\) | +| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) | +| ↳ `team` | object | Team object | +| ↳ `id` | string | Team ID | +| ↳ `name` | string | Team name | ### `linear_list_cycles` @@ -935,6 +965,7 @@ List cycles (sprints/iterations) in Linear | ↳ `endsAt` | string | End date \(ISO 8601\) | | ↳ `completedAt` | string | Completion date \(ISO 8601\) | | ↳ `progress` | number | Progress percentage \(0-1\) | +| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | | ↳ `team` | object | Team object | | ↳ `id` | string | Team ID | | ↳ `name` | string | Team name | @@ -961,6 +992,7 @@ Get a single cycle by ID from Linear | ↳ `endsAt` | string | End date \(ISO 8601\) | | ↳ `completedAt` | string | Completion date \(ISO 8601\) | | ↳ `progress` | number | Progress percentage \(0-1\) | +| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | | ↳ `team` | object | Team object | | ↳ `id` | string | Team ID | | ↳ `name` | string | Team name | @@ -986,9 +1018,14 @@ Create a new cycle (sprint/iteration) in Linear | ↳ `id` | string | Cycle ID | | ↳ `number` | number | Cycle number | | ↳ `name` | string | Cycle name | -| ↳ `startsAt` | string | Start date | -| ↳ `endsAt` | string | End date | -| ↳ `team` | object | Team this cycle belongs to | +| ↳ `startsAt` | string | Start date \(ISO 8601\) | +| ↳ `endsAt` | string | End date \(ISO 8601\) | +| ↳ `completedAt` | string | Completion date \(ISO 8601\) | +| ↳ `progress` | number | Progress percentage \(0-1\) | +| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | +| ↳ `team` | object | Team object | +| ↳ `id` | string | Team ID | +| ↳ `name` | string | Team name | ### `linear_get_active_cycle` @@ -1008,10 +1045,14 @@ Get the currently active cycle for a team | ↳ `id` | string | Cycle ID | | ↳ `number` | number | Cycle number | | ↳ `name` | string | Cycle name | -| ↳ `startsAt` | string | Start date | -| ↳ `endsAt` | string | End date | -| ↳ `progress` | number | Progress percentage | -| ↳ `team` | object | Team this cycle belongs to | +| ↳ `startsAt` | string | Start date \(ISO 8601\) | +| ↳ `endsAt` | string | End date \(ISO 8601\) | +| ↳ `completedAt` | string | Completion date \(ISO 8601\) | +| ↳ `progress` | number | Progress percentage \(0-1\) | +| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | +| ↳ `team` | object | Team object | +| ↳ `id` | string | Team ID | +| ↳ `name` | string | Team name | ### `linear_create_attachment` @@ -1334,8 +1375,12 @@ Create a new customer in Linear | ↳ `domains` | array | Associated domains | | ↳ `externalIds` | array | External IDs from other systems | | ↳ `logoUrl` | string | Logo URL | +| ↳ `slugId` | string | Unique URL slug | | ↳ `approximateNeedCount` | number | Number of customer needs | +| ↳ `revenue` | number | Annual revenue | +| ↳ `size` | number | Organization size | | ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | +| ↳ `updatedAt` | string | Last update timestamp \(ISO 8601\) | | ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) | ### `linear_list_customers` @@ -1363,8 +1408,12 @@ List all customers in Linear | ↳ `domains` | array | Associated domains | | ↳ `externalIds` | array | External IDs from other systems | | ↳ `logoUrl` | string | Logo URL | +| ↳ `slugId` | string | Unique URL slug | | ↳ `approximateNeedCount` | number | Number of customer needs | +| ↳ `revenue` | number | Annual revenue | +| ↳ `size` | number | Organization size | | ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | +| ↳ `updatedAt` | string | Last update timestamp \(ISO 8601\) | | ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) | ### `linear_create_customer_request` @@ -1480,8 +1529,12 @@ Get a single customer by ID in Linear | ↳ `domains` | array | Associated domains | | ↳ `externalIds` | array | External IDs from other systems | | ↳ `logoUrl` | string | Logo URL | +| ↳ `slugId` | string | Unique URL slug | | ↳ `approximateNeedCount` | number | Number of customer needs | +| ↳ `revenue` | number | Annual revenue | +| ↳ `size` | number | Organization size | | ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | +| ↳ `updatedAt` | string | Last update timestamp \(ISO 8601\) | | ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) | ### `linear_update_customer` @@ -1513,8 +1566,12 @@ Update a customer in Linear | ↳ `domains` | array | Associated domains | | ↳ `externalIds` | array | External IDs from other systems | | ↳ `logoUrl` | string | Logo URL | +| ↳ `slugId` | string | Unique URL slug | | ↳ `approximateNeedCount` | number | Number of customer needs | +| ↳ `revenue` | number | Annual revenue | +| ↳ `size` | number | Organization size | | ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | +| ↳ `updatedAt` | string | Last update timestamp \(ISO 8601\) | | ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) | ### `linear_delete_customer` @@ -1560,8 +1617,8 @@ Create a new customer status in Linear | --------- | ---- | -------- | ----------- | | `name` | string | Yes | Customer status name | | `color` | string | Yes | Status color \(hex code\) | -| `displayName` | string | No | Display name for the status | | `description` | string | No | Status description | +| `displayName` | string | No | Display name for the status | | `position` | number | No | Position in status list | #### Output @@ -1571,11 +1628,12 @@ Create a new customer status in Linear | `customerStatus` | object | The created customer status | | ↳ `id` | string | Customer status ID | | ↳ `name` | string | Status name | -| ↳ `displayName` | string | Display name | | ↳ `description` | string | Status description | | ↳ `color` | string | Status color \(hex\) | | ↳ `position` | number | Position in list | +| ↳ `type` | string | Status type \(active, inactive\) | | ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | +| ↳ `updatedAt` | string | Last updated timestamp \(ISO 8601\) | | ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) | ### `linear_update_customer_status` @@ -1589,8 +1647,8 @@ Update a customer status in Linear | `statusId` | string | Yes | Customer status ID to update | | `name` | string | No | Updated status name | | `color` | string | No | Updated status color | -| `displayName` | string | No | Updated display name | | `description` | string | No | Updated description | +| `displayName` | string | No | Updated display name | | `position` | number | No | Updated position | #### Output @@ -1598,6 +1656,15 @@ Update a customer status in Linear | Parameter | Type | Description | | --------- | ---- | ----------- | | `customerStatus` | object | The updated customer status | +| ↳ `id` | string | Customer status ID | +| ↳ `name` | string | Status name | +| ↳ `description` | string | Status description | +| ↳ `color` | string | Status color \(hex\) | +| ↳ `position` | number | Position in list | +| ↳ `type` | string | Status type \(active, inactive\) | +| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | +| ↳ `updatedAt` | string | Last updated timestamp \(ISO 8601\) | +| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) | ### `linear_delete_customer_status` @@ -1623,19 +1690,25 @@ List all customer statuses in Linear | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `first` | number | No | Number of statuses to return \(default: 50\) | +| `after` | string | No | Cursor for pagination | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | +| `pageInfo` | object | Pagination information | +| ↳ `hasNextPage` | boolean | Whether there are more results | +| ↳ `endCursor` | string | Cursor for the next page | | `customerStatuses` | array | List of customer statuses | | ↳ `id` | string | Customer status ID | | ↳ `name` | string | Status name | -| ↳ `displayName` | string | Display name | | ↳ `description` | string | Status description | | ↳ `color` | string | Status color \(hex\) | | ↳ `position` | number | Position in list | +| ↳ `type` | string | Status type \(active, inactive\) | | ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | +| ↳ `updatedAt` | string | Last updated timestamp \(ISO 8601\) | | ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) | ### `linear_create_customer_tier` @@ -1711,11 +1784,16 @@ List all customer tiers in Linear | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `first` | number | No | Number of tiers to return \(default: 50\) | +| `after` | string | No | Cursor for pagination | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | +| `pageInfo` | object | Pagination information | +| ↳ `hasNextPage` | boolean | Whether there are more results | +| ↳ `endCursor` | string | Cursor for the next page | | `customerTiers` | array | List of customer tiers | | ↳ `id` | string | Customer tier ID | | ↳ `name` | string | Tier name | @@ -1761,6 +1839,14 @@ Create a new project label in Linear | Parameter | Type | Description | | --------- | ---- | ----------- | | `projectLabel` | object | The created project label | +| ↳ `id` | string | Project label ID | +| ↳ `name` | string | Label name | +| ↳ `description` | string | Label description | +| ↳ `color` | string | Label color \(hex\) | +| ↳ `isGroup` | boolean | Whether this label is a group | +| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | +| ↳ `updatedAt` | string | Last update timestamp \(ISO 8601\) | +| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) | ### `linear_update_project_label` @@ -1780,6 +1866,14 @@ Update a project label in Linear | Parameter | Type | Description | | --------- | ---- | ----------- | | `projectLabel` | object | The updated project label | +| ↳ `id` | string | Project label ID | +| ↳ `name` | string | Label name | +| ↳ `description` | string | Label description | +| ↳ `color` | string | Label color \(hex\) | +| ↳ `isGroup` | boolean | Whether this label is a group | +| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | +| ↳ `updatedAt` | string | Last update timestamp \(ISO 8601\) | +| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) | ### `linear_delete_project_label` @@ -1806,12 +1900,25 @@ List all project labels in Linear | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `projectId` | string | No | Optional project ID to filter labels for a specific project | +| `first` | number | No | Number of labels to return \(default: 50\) | +| `after` | string | No | Cursor for pagination | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | +| `pageInfo` | object | Pagination information | +| ↳ `hasNextPage` | boolean | Whether there are more results | +| ↳ `endCursor` | string | Cursor for the next page | | `projectLabels` | array | List of project labels | +| ↳ `id` | string | Project label ID | +| ↳ `name` | string | Label name | +| ↳ `description` | string | Label description | +| ↳ `color` | string | Label color \(hex\) | +| ↳ `isGroup` | boolean | Whether this label is a group | +| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | +| ↳ `updatedAt` | string | Last update timestamp \(ISO 8601\) | +| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) | ### `linear_add_label_to_project` @@ -1867,6 +1974,16 @@ Create a new project milestone in Linear | Parameter | Type | Description | | --------- | ---- | ----------- | | `projectMilestone` | object | The created project milestone | +| ↳ `id` | string | Project milestone ID | +| ↳ `name` | string | Milestone name | +| ↳ `description` | string | Milestone description | +| ↳ `projectId` | string | Project ID | +| ↳ `targetDate` | string | Target date \(YYYY-MM-DD\) | +| ↳ `progress` | number | Progress percentage \(0-1\) | +| ↳ `sortOrder` | number | Sort order within the project | +| ↳ `status` | string | Milestone status \(done, next, overdue, unstarted\) | +| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | +| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) | ### `linear_update_project_milestone` @@ -1886,6 +2003,16 @@ Update a project milestone in Linear | Parameter | Type | Description | | --------- | ---- | ----------- | | `projectMilestone` | object | The updated project milestone | +| ↳ `id` | string | Project milestone ID | +| ↳ `name` | string | Milestone name | +| ↳ `description` | string | Milestone description | +| ↳ `projectId` | string | Project ID | +| ↳ `targetDate` | string | Target date \(YYYY-MM-DD\) | +| ↳ `progress` | number | Progress percentage \(0-1\) | +| ↳ `sortOrder` | number | Sort order within the project | +| ↳ `status` | string | Milestone status \(done, next, overdue, unstarted\) | +| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | +| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) | ### `linear_delete_project_milestone` @@ -1912,12 +2039,27 @@ List all milestones for a project in Linear | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `projectId` | string | Yes | Project ID to list milestones for | +| `first` | number | No | Number of milestones to return \(default: 50\) | +| `after` | string | No | Cursor for pagination | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | +| `pageInfo` | object | Pagination information | +| ↳ `hasNextPage` | boolean | Whether there are more results | +| ↳ `endCursor` | string | Cursor for the next page | | `projectMilestones` | array | List of project milestones | +| ↳ `id` | string | Project milestone ID | +| ↳ `name` | string | Milestone name | +| ↳ `description` | string | Milestone description | +| ↳ `projectId` | string | Project ID | +| ↳ `targetDate` | string | Target date \(YYYY-MM-DD\) | +| ↳ `progress` | number | Progress percentage \(0-1\) | +| ↳ `sortOrder` | number | Sort order within the project | +| ↳ `status` | string | Milestone status \(done, next, overdue, unstarted\) | +| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | +| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) | ### `linear_create_project_status` @@ -1939,6 +2081,16 @@ Create a new project status in Linear | Parameter | Type | Description | | --------- | ---- | ----------- | | `projectStatus` | object | The created project status | +| ↳ `id` | string | Project status ID | +| ↳ `name` | string | Status name | +| ↳ `description` | string | Status description | +| ↳ `color` | string | Status color \(hex\) | +| ↳ `indefinite` | boolean | Whether this status is indefinite | +| ↳ `position` | number | Position in list | +| ↳ `type` | string | Status type \(backlog, planned, started, paused, completed, canceled\) | +| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | +| ↳ `updatedAt` | string | Last updated timestamp \(ISO 8601\) | +| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) | ### `linear_update_project_status` @@ -1960,6 +2112,16 @@ Update a project status in Linear | Parameter | Type | Description | | --------- | ---- | ----------- | | `projectStatus` | object | The updated project status | +| ↳ `id` | string | Project status ID | +| ↳ `name` | string | Status name | +| ↳ `description` | string | Status description | +| ↳ `color` | string | Status color \(hex\) | +| ↳ `indefinite` | boolean | Whether this status is indefinite | +| ↳ `position` | number | Position in list | +| ↳ `type` | string | Status type \(backlog, planned, started, paused, completed, canceled\) | +| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | +| ↳ `updatedAt` | string | Last updated timestamp \(ISO 8601\) | +| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) | ### `linear_delete_project_status` @@ -1985,11 +2147,26 @@ List all project statuses in Linear | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `first` | number | No | Number of statuses to return \(default: 50\) | +| `after` | string | No | Cursor for pagination | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | +| `pageInfo` | object | Pagination information | +| ↳ `hasNextPage` | boolean | Whether there are more results | +| ↳ `endCursor` | string | Cursor for the next page | | `projectStatuses` | array | List of project statuses | +| ↳ `id` | string | Project status ID | +| ↳ `name` | string | Status name | +| ↳ `description` | string | Status description | +| ↳ `color` | string | Status color \(hex\) | +| ↳ `indefinite` | boolean | Whether this status is indefinite | +| ↳ `position` | number | Position in list | +| ↳ `type` | string | Status type \(backlog, planned, started, paused, completed, canceled\) | +| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | +| ↳ `updatedAt` | string | Last updated timestamp \(ISO 8601\) | +| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) | diff --git a/apps/sim/blocks/blocks/linear.ts b/apps/sim/blocks/blocks/linear.ts index 4774f7fe1..b7b838ef2 100644 --- a/apps/sim/blocks/blocks/linear.ts +++ b/apps/sim/blocks/blocks/linear.ts @@ -810,7 +810,29 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n placeholder: 'Number of items to return (default: 50)', condition: { field: 'operation', - value: ['linear_list_favorites'], + value: [ + 'linear_read_issues', + 'linear_search_issues', + 'linear_list_comments', + 'linear_list_projects', + 'linear_list_users', + 'linear_list_teams', + 'linear_list_labels', + 'linear_list_workflow_states', + 'linear_list_cycles', + 'linear_list_attachments', + 'linear_list_issue_relations', + 'linear_list_favorites', + 'linear_list_project_updates', + 'linear_list_notifications', + 'linear_list_customer_statuses', + 'linear_list_customer_tiers', + 'linear_list_customers', + 'linear_list_customer_requests', + 'linear_list_project_labels', + 'linear_list_project_milestones', + 'linear_list_project_statuses', + ], }, }, // Pagination - After (for list operations) @@ -821,7 +843,29 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n placeholder: 'Cursor for pagination', condition: { field: 'operation', - value: ['linear_list_favorites'], + value: [ + 'linear_read_issues', + 'linear_search_issues', + 'linear_list_comments', + 'linear_list_projects', + 'linear_list_users', + 'linear_list_teams', + 'linear_list_labels', + 'linear_list_workflow_states', + 'linear_list_cycles', + 'linear_list_attachments', + 'linear_list_issue_relations', + 'linear_list_favorites', + 'linear_list_project_updates', + 'linear_list_notifications', + 'linear_list_customers', + 'linear_list_customer_requests', + 'linear_list_customer_statuses', + 'linear_list_customer_tiers', + 'linear_list_project_labels', + 'linear_list_project_milestones', + 'linear_list_project_statuses', + ], }, }, // Project health (for project updates) @@ -1053,28 +1097,6 @@ Return ONLY the description text - no explanations.`, value: ['linear_create_customer_request', 'linear_update_customer_request'], }, }, - // Pagination - first - { - id: 'first', - title: 'Limit', - type: 'short-input', - placeholder: 'Number of items (default: 50)', - condition: { - field: 'operation', - value: ['linear_list_customers', 'linear_list_customer_requests'], - }, - }, - // Pagination - after - { - id: 'after', - title: 'After Cursor', - type: 'short-input', - placeholder: 'Cursor for pagination', - condition: { - field: 'operation', - value: ['linear_list_customers', 'linear_list_customer_requests'], - }, - }, // Customer ID for get/update/delete/merge operations { id: 'customerIdTarget', @@ -1493,6 +1515,8 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n teamId: effectiveTeamId || undefined, projectId: effectiveProjectId || undefined, includeArchived: params.includeArchived, + first: params.first ? Number(params.first) : undefined, + after: params.after, } case 'linear_get_issue': @@ -1558,6 +1582,8 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n query: params.query.trim(), teamId: effectiveTeamId, includeArchived: params.includeArchived, + first: params.first ? Number(params.first) : undefined, + after: params.after, } case 'linear_add_label_to_issue': @@ -1607,6 +1633,8 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n return { ...baseParams, issueId: params.issueId.trim(), + first: params.first ? Number(params.first) : undefined, + after: params.after, } case 'linear_list_projects': @@ -1614,6 +1642,8 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n ...baseParams, teamId: effectiveTeamId, includeArchived: params.includeArchived, + first: params.first ? Number(params.first) : undefined, + after: params.after, } case 'linear_get_project': @@ -1665,6 +1695,12 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n case 'linear_list_users': case 'linear_list_teams': + return { + ...baseParams, + first: params.first ? Number(params.first) : undefined, + after: params.after, + } + case 'linear_get_viewer': return baseParams @@ -1672,6 +1708,8 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n return { ...baseParams, teamId: effectiveTeamId, + first: params.first ? Number(params.first) : undefined, + after: params.after, } case 'linear_create_label': @@ -1709,6 +1747,8 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n return { ...baseParams, teamId: effectiveTeamId, + first: params.first ? Number(params.first) : undefined, + after: params.after, } case 'linear_create_workflow_state': @@ -1738,6 +1778,8 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n return { ...baseParams, teamId: effectiveTeamId, + first: params.first ? Number(params.first) : undefined, + after: params.after, } case 'linear_get_cycle': @@ -1801,6 +1843,8 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n return { ...baseParams, issueId: params.issueId.trim(), + first: params.first ? Number(params.first) : undefined, + after: params.after, } case 'linear_update_attachment': @@ -1840,6 +1884,8 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n return { ...baseParams, issueId: params.issueId.trim(), + first: params.first ? Number(params.first) : undefined, + after: params.after, } case 'linear_delete_issue_relation': @@ -1886,10 +1932,16 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n return { ...baseParams, projectId: effectiveProjectId, + first: params.first ? Number(params.first) : undefined, + after: params.after, } case 'linear_list_notifications': - return baseParams + return { + ...baseParams, + first: params.first ? Number(params.first) : undefined, + after: params.after, + } case 'linear_update_notification': if (!params.notificationId?.trim()) { @@ -2018,9 +2070,9 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n return { ...baseParams, name: params.statusName.trim(), - displayName: params.statusDisplayName?.trim() || params.statusName.trim(), color: params.statusColor.trim(), description: params.statusDescription?.trim() || undefined, + displayName: params.statusDisplayName?.trim() || undefined, } case 'linear_update_customer_status': @@ -2031,9 +2083,9 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n ...baseParams, statusId: params.statusId.trim(), name: params.statusName?.trim() || undefined, - displayName: params.statusDisplayName?.trim() || undefined, color: params.statusColor?.trim() || undefined, description: params.statusDescription?.trim() || undefined, + displayName: params.statusDisplayName?.trim() || undefined, } case 'linear_delete_customer_status': @@ -2046,7 +2098,11 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n } case 'linear_list_customer_statuses': - return baseParams + return { + ...baseParams, + first: params.first ? Number(params.first) : undefined, + after: params.after, + } // Customer Tier Operations case 'linear_create_customer_tier': @@ -2084,7 +2140,11 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n } case 'linear_list_customer_tiers': - return baseParams + return { + ...baseParams, + first: params.first ? Number(params.first) : undefined, + after: params.after, + } // Project Management Operations case 'linear_delete_project': @@ -2135,6 +2195,8 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n return { ...baseParams, projectId: effectiveProjectId || undefined, + first: params.first ? Number(params.first) : undefined, + after: params.after, } case 'linear_add_label_to_project': @@ -2198,6 +2260,8 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n return { ...baseParams, projectId: params.projectIdForMilestone.trim(), + first: params.first ? Number(params.first) : undefined, + after: params.after, } // Project Status Operations @@ -2245,7 +2309,11 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n } case 'linear_list_project_statuses': - return baseParams + return { + ...baseParams, + first: params.first ? Number(params.first) : undefined, + after: params.after, + } default: return baseParams @@ -2321,9 +2389,9 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n // Customer status and tier inputs statusId: { type: 'string', description: 'Status identifier' }, statusName: { type: 'string', description: 'Status name' }, - statusDisplayName: { type: 'string', description: 'Status display name' }, statusColor: { type: 'string', description: 'Status color in hex format' }, statusDescription: { type: 'string', description: 'Status description' }, + statusDisplayName: { type: 'string', description: 'Status display name' }, tierId: { type: 'string', description: 'Tier identifier' }, tierName: { type: 'string', description: 'Tier name' }, tierDisplayName: { type: 'string', description: 'Tier display name' }, diff --git a/apps/sim/tools/linear/create_customer.ts b/apps/sim/tools/linear/create_customer.ts index 1b286d178..241a9c8dc 100644 --- a/apps/sim/tools/linear/create_customer.ts +++ b/apps/sim/tools/linear/create_customer.ts @@ -131,8 +131,12 @@ export const linearCreateCustomerTool: ToolConfig< domains externalIds logoUrl + slugId approximateNeedCount + revenue + size createdAt + updatedAt archivedAt } } diff --git a/apps/sim/tools/linear/create_customer_status.ts b/apps/sim/tools/linear/create_customer_status.ts index ef1869f2d..5e1467d5e 100644 --- a/apps/sim/tools/linear/create_customer_status.ts +++ b/apps/sim/tools/linear/create_customer_status.ts @@ -32,18 +32,18 @@ export const linearCreateCustomerStatusTool: ToolConfig< visibility: 'user-or-llm', description: 'Status color (hex code)', }, - displayName: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'Display name for the status', - }, description: { type: 'string', required: false, visibility: 'user-or-llm', description: 'Status description', }, + displayName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Display name for the status', + }, position: { type: 'number', required: false, @@ -70,12 +70,12 @@ export const linearCreateCustomerStatusTool: ToolConfig< color: params.color, } - if (params.displayName != null && params.displayName !== '') { - input.displayName = params.displayName - } if (params.description != null && params.description !== '') { input.description = params.description } + if (params.displayName != null && params.displayName !== '') { + input.displayName = params.displayName + } if (params.position != null) { input.position = params.position } @@ -88,11 +88,12 @@ export const linearCreateCustomerStatusTool: ToolConfig< status { id name - displayName description color position + type createdAt + updatedAt archivedAt } } diff --git a/apps/sim/tools/linear/create_cycle.ts b/apps/sim/tools/linear/create_cycle.ts index 81974a310..da3d67b55 100644 --- a/apps/sim/tools/linear/create_cycle.ts +++ b/apps/sim/tools/linear/create_cycle.ts @@ -1,4 +1,5 @@ import type { LinearCreateCycleParams, LinearCreateCycleResponse } from '@/tools/linear/types' +import { CYCLE_FULL_OUTPUT_PROPERTIES } from '@/tools/linear/types' import type { ToolConfig } from '@/tools/types' export const linearCreateCycleTool: ToolConfig = @@ -72,7 +73,9 @@ export const linearCreateCycleTool: ToolConfig ({ + body: (params) => ({ query: ` - query CustomerStatuses { - customerStatuses { + query CustomerStatuses($first: Int, $after: String) { + customerStatuses(first: $first, after: $after) { nodes { id name - displayName description color position + type createdAt + updatedAt archivedAt } + pageInfo { + hasNextPage + endCursor + } } } `, + variables: { + first: params.first ? Number(params.first) : 50, + after: params.after, + }, }), }, @@ -64,10 +86,15 @@ export const linearListCustomerStatusesTool: ToolConfig< } } + const result = data.data.customerStatuses return { success: true, output: { - customerStatuses: data.data.customerStatuses.nodes, + customerStatuses: result.nodes, + pageInfo: { + hasNextPage: result.pageInfo.hasNextPage, + endCursor: result.pageInfo.endCursor, + }, }, } }, @@ -81,5 +108,6 @@ export const linearListCustomerStatusesTool: ToolConfig< properties: CUSTOMER_STATUS_OUTPUT_PROPERTIES, }, }, + pageInfo: PAGE_INFO_OUTPUT, }, } diff --git a/apps/sim/tools/linear/list_customer_tiers.ts b/apps/sim/tools/linear/list_customer_tiers.ts index ee699912f..5b16c968d 100644 --- a/apps/sim/tools/linear/list_customer_tiers.ts +++ b/apps/sim/tools/linear/list_customer_tiers.ts @@ -2,7 +2,7 @@ import type { LinearListCustomerTiersParams, LinearListCustomerTiersResponse, } from '@/tools/linear/types' -import { CUSTOMER_TIER_OUTPUT_PROPERTIES } from '@/tools/linear/types' +import { CUSTOMER_TIER_OUTPUT_PROPERTIES, PAGE_INFO_OUTPUT } from '@/tools/linear/types' import type { ToolConfig } from '@/tools/types' export const linearListCustomerTiersTool: ToolConfig< @@ -19,7 +19,20 @@ export const linearListCustomerTiersTool: ToolConfig< provider: 'linear', }, - params: {}, + params: { + first: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of tiers to return (default: 50)', + }, + after: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Cursor for pagination', + }, + }, request: { url: 'https://api.linear.app/graphql', @@ -33,10 +46,10 @@ export const linearListCustomerTiersTool: ToolConfig< Authorization: `Bearer ${params.accessToken}`, } }, - body: () => ({ + body: (params) => ({ query: ` - query CustomerTiers { - customerTiers { + query CustomerTiers($first: Int, $after: String) { + customerTiers(first: $first, after: $after) { nodes { id name @@ -47,9 +60,17 @@ export const linearListCustomerTiersTool: ToolConfig< createdAt archivedAt } + pageInfo { + hasNextPage + endCursor + } } } `, + variables: { + first: params.first ? Number(params.first) : 50, + after: params.after, + }, }), }, @@ -64,10 +85,15 @@ export const linearListCustomerTiersTool: ToolConfig< } } + const result = data.data.customerTiers return { success: true, output: { - customerTiers: data.data.customerTiers.nodes, + customerTiers: result.nodes, + pageInfo: { + hasNextPage: result.pageInfo.hasNextPage, + endCursor: result.pageInfo.endCursor, + }, }, } }, @@ -81,5 +107,6 @@ export const linearListCustomerTiersTool: ToolConfig< properties: CUSTOMER_TIER_OUTPUT_PROPERTIES, }, }, + pageInfo: PAGE_INFO_OUTPUT, }, } diff --git a/apps/sim/tools/linear/list_customers.ts b/apps/sim/tools/linear/list_customers.ts index 9248cec58..4aa4fe75a 100644 --- a/apps/sim/tools/linear/list_customers.ts +++ b/apps/sim/tools/linear/list_customers.ts @@ -59,8 +59,12 @@ export const linearListCustomersTool: ToolConfig< domains externalIds logoUrl + slugId approximateNeedCount + revenue + size createdAt + updatedAt archivedAt } pageInfo { @@ -71,7 +75,7 @@ export const linearListCustomersTool: ToolConfig< } `, variables: { - first: params.first || 50, + first: params.first ? Number(params.first) : 50, after: params.after, includeArchived: params.includeArchived || false, }, diff --git a/apps/sim/tools/linear/list_cycles.ts b/apps/sim/tools/linear/list_cycles.ts index 90bcf7990..b351bc77b 100644 --- a/apps/sim/tools/linear/list_cycles.ts +++ b/apps/sim/tools/linear/list_cycles.ts @@ -64,6 +64,7 @@ export const linearListCyclesTool: ToolConfig { - // If projectId is provided, query the specific project's labels if (params.projectId?.trim()) { return { query: ` - query ProjectWithLabels($id: String!) { + query ProjectWithLabels($id: String!, $first: Int, $after: String) { project(id: $id) { id name - labels { + labels(first: $first, after: $after) { nodes { id name @@ -56,23 +68,29 @@ export const linearListProjectLabelsTool: ToolConfig< color isGroup createdAt + updatedAt archivedAt } + pageInfo { + hasNextPage + endCursor + } } } } `, variables: { id: params.projectId.trim(), + first: params.first ? Number(params.first) : 50, + after: params.after, }, } } - // Otherwise, list all project labels return { query: ` - query ProjectLabels { - projectLabels { + query ProjectLabels($first: Int, $after: String) { + projectLabels(first: $first, after: $after) { nodes { id name @@ -80,11 +98,20 @@ export const linearListProjectLabelsTool: ToolConfig< color isGroup createdAt + updatedAt archivedAt } + pageInfo { + hasNextPage + endCursor + } } } `, + variables: { + first: params.first ? Number(params.first) : 50, + after: params.after, + }, } }, }, @@ -100,21 +127,29 @@ export const linearListProjectLabelsTool: ToolConfig< } } - // Handle project-specific query response if (data.data.project) { + const result = data.data.project.labels return { success: true, output: { - projectLabels: data.data.project.labels.nodes, + projectLabels: result.nodes, + pageInfo: { + hasNextPage: result.pageInfo.hasNextPage, + endCursor: result.pageInfo.endCursor, + }, }, } } - // Handle global projectLabels query response + const result = data.data.projectLabels return { success: true, output: { - projectLabels: data.data.projectLabels.nodes, + projectLabels: result.nodes, + pageInfo: { + hasNextPage: result.pageInfo.hasNextPage, + endCursor: result.pageInfo.endCursor, + }, }, } }, @@ -123,6 +158,11 @@ export const linearListProjectLabelsTool: ToolConfig< projectLabels: { type: 'array', description: 'List of project labels', + items: { + type: 'object', + properties: PROJECT_LABEL_OUTPUT_PROPERTIES, + }, }, + pageInfo: PAGE_INFO_OUTPUT, }, } diff --git a/apps/sim/tools/linear/list_project_milestones.ts b/apps/sim/tools/linear/list_project_milestones.ts index 7a80bbb69..afcde75c2 100644 --- a/apps/sim/tools/linear/list_project_milestones.ts +++ b/apps/sim/tools/linear/list_project_milestones.ts @@ -2,6 +2,7 @@ import type { LinearListProjectMilestonesParams, LinearListProjectMilestonesResponse, } from '@/tools/linear/types' +import { PAGE_INFO_OUTPUT, PROJECT_MILESTONE_OUTPUT_PROPERTIES } from '@/tools/linear/types' import type { ToolConfig } from '@/tools/types' export const linearListProjectMilestonesTool: ToolConfig< @@ -25,6 +26,18 @@ export const linearListProjectMilestonesTool: ToolConfig< visibility: 'user-or-llm', description: 'Project ID to list milestones for', }, + first: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of milestones to return (default: 50)', + }, + after: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Cursor for pagination', + }, }, request: { @@ -41,17 +54,26 @@ export const linearListProjectMilestonesTool: ToolConfig< }, body: (params) => ({ query: ` - query Project($id: String!) { + query Project($id: String!, $first: Int, $after: String) { project(id: $id) { - projectMilestones { + projectMilestones(first: $first, after: $after) { nodes { id name description - projectId targetDate + progress + sortOrder + status createdAt archivedAt + project { + id + } + } + pageInfo { + hasNextPage + endCursor } } } @@ -59,6 +81,8 @@ export const linearListProjectMilestonesTool: ToolConfig< `, variables: { id: params.projectId, + first: params.first ? Number(params.first) : 50, + after: params.after, }, }), }, @@ -74,10 +98,20 @@ export const linearListProjectMilestonesTool: ToolConfig< } } + const result = data.data.project?.projectMilestones + const milestones = (result?.nodes || []).map((node: Record) => ({ + ...node, + projectId: (node.project as Record)?.id ?? null, + project: undefined, + })) return { success: true, output: { - projectMilestones: data.data.project?.projectMilestones?.nodes || [], + projectMilestones: milestones, + pageInfo: { + hasNextPage: result?.pageInfo?.hasNextPage ?? false, + endCursor: result?.pageInfo?.endCursor, + }, }, } }, @@ -86,6 +120,11 @@ export const linearListProjectMilestonesTool: ToolConfig< projectMilestones: { type: 'array', description: 'List of project milestones', + items: { + type: 'object', + properties: PROJECT_MILESTONE_OUTPUT_PROPERTIES, + }, }, + pageInfo: PAGE_INFO_OUTPUT, }, } diff --git a/apps/sim/tools/linear/list_project_statuses.ts b/apps/sim/tools/linear/list_project_statuses.ts index 1ccaa8bb7..c0266c1a5 100644 --- a/apps/sim/tools/linear/list_project_statuses.ts +++ b/apps/sim/tools/linear/list_project_statuses.ts @@ -2,6 +2,7 @@ import type { LinearListProjectStatusesParams, LinearListProjectStatusesResponse, } from '@/tools/linear/types' +import { PAGE_INFO_OUTPUT, PROJECT_STATUS_OUTPUT_PROPERTIES } from '@/tools/linear/types' import type { ToolConfig } from '@/tools/types' export const linearListProjectStatusesTool: ToolConfig< @@ -18,7 +19,20 @@ export const linearListProjectStatusesTool: ToolConfig< provider: 'linear', }, - params: {}, + params: { + first: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of statuses to return (default: 50)', + }, + after: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Cursor for pagination', + }, + }, request: { url: 'https://api.linear.app/graphql', @@ -32,10 +46,10 @@ export const linearListProjectStatusesTool: ToolConfig< Authorization: `Bearer ${params.accessToken}`, } }, - body: () => ({ + body: (params) => ({ query: ` - query ProjectStatuses { - projectStatuses { + query ProjectStatuses($first: Int, $after: String) { + projectStatuses(first: $first, after: $after) { nodes { id name @@ -43,12 +57,22 @@ export const linearListProjectStatusesTool: ToolConfig< color indefinite position + type createdAt + updatedAt archivedAt } + pageInfo { + hasNextPage + endCursor + } } } `, + variables: { + first: params.first ? Number(params.first) : 50, + after: params.after, + }, }), }, @@ -63,10 +87,15 @@ export const linearListProjectStatusesTool: ToolConfig< } } + const result = data.data.projectStatuses return { success: true, output: { - projectStatuses: data.data.projectStatuses.nodes, + projectStatuses: result.nodes, + pageInfo: { + hasNextPage: result.pageInfo.hasNextPage, + endCursor: result.pageInfo.endCursor, + }, }, } }, @@ -75,6 +104,11 @@ export const linearListProjectStatusesTool: ToolConfig< projectStatuses: { type: 'array', description: 'List of project statuses', + items: { + type: 'object', + properties: PROJECT_STATUS_OUTPUT_PROPERTIES, + }, }, + pageInfo: PAGE_INFO_OUTPUT, }, } diff --git a/apps/sim/tools/linear/list_projects.ts b/apps/sim/tools/linear/list_projects.ts index 5e5d881f2..a9700f324 100644 --- a/apps/sim/tools/linear/list_projects.ts +++ b/apps/sim/tools/linear/list_projects.ts @@ -93,7 +93,7 @@ export const linearListProjectsTool: ToolConfig< } `, variables: { - first: params.first || 50, + first: params.first ? Number(params.first) : 50, after: params.after, includeArchived: params.includeArchived || false, }, diff --git a/apps/sim/tools/linear/list_workflow_states.ts b/apps/sim/tools/linear/list_workflow_states.ts index b24e72c4c..dffb5fcb4 100644 --- a/apps/sim/tools/linear/list_workflow_states.ts +++ b/apps/sim/tools/linear/list_workflow_states.ts @@ -65,9 +65,13 @@ export const linearListWorkflowStatesTool: ToolConfig< nodes { id name + description type color position + createdAt + updatedAt + archivedAt team { id name diff --git a/apps/sim/tools/linear/search_issues.ts b/apps/sim/tools/linear/search_issues.ts index 8ec097792..33f77730a 100644 --- a/apps/sim/tools/linear/search_issues.ts +++ b/apps/sim/tools/linear/search_issues.ts @@ -41,6 +41,12 @@ export const linearSearchIssuesTool: ToolConfig< visibility: 'user-or-llm', description: 'Number of results to return (default: 50)', }, + after: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Cursor for pagination', + }, }, request: { @@ -63,8 +69,8 @@ export const linearSearchIssuesTool: ToolConfig< return { query: ` - query SearchIssues($term: String!, $filter: IssueFilter, $first: Int, $includeArchived: Boolean) { - searchIssues(term: $term, filter: $filter, first: $first, includeArchived: $includeArchived) { + query SearchIssues($term: String!, $filter: IssueFilter, $first: Int, $after: String, $includeArchived: Boolean) { + searchIssues(term: $term, filter: $filter, first: $first, after: $after, includeArchived: $includeArchived) { nodes { id title @@ -111,7 +117,8 @@ export const linearSearchIssuesTool: ToolConfig< variables: { term: params.query, filter: Object.keys(filter).length > 0 ? filter : undefined, - first: params.first || 50, + first: params.first ? Number(params.first) : 50, + after: params.after, includeArchived: params.includeArchived || false, }, } diff --git a/apps/sim/tools/linear/types.ts b/apps/sim/tools/linear/types.ts index b66bae5b4..5c3f644c2 100644 --- a/apps/sim/tools/linear/types.ts +++ b/apps/sim/tools/linear/types.ts @@ -112,6 +112,10 @@ export const LABEL_FULL_OUTPUT_PROPERTIES = { name: { type: 'string', description: 'Label name' }, color: { type: 'string', description: 'Label color (hex)' }, description: { type: 'string', description: 'Label description' }, + isGroup: { type: 'boolean', description: 'Whether this label is a group' }, + createdAt: { type: 'string', description: 'Creation timestamp (ISO 8601)' }, + updatedAt: { type: 'string', description: 'Last update timestamp (ISO 8601)' }, + archivedAt: { type: 'string', description: 'Archive timestamp (ISO 8601)' }, team: TEAM_OUTPUT, } as const satisfies Record @@ -144,6 +148,7 @@ export const CYCLE_FULL_OUTPUT_PROPERTIES = { endsAt: { type: 'string', description: 'End date (ISO 8601)' }, completedAt: { type: 'string', description: 'Completion date (ISO 8601)' }, progress: { type: 'number', description: 'Progress percentage (0-1)' }, + createdAt: { type: 'string', description: 'Creation timestamp (ISO 8601)' }, team: TEAM_OUTPUT, } as const satisfies Record @@ -277,9 +282,16 @@ export const ATTACHMENT_OUTPUT_PROPERTIES = { export const WORKFLOW_STATE_OUTPUT_PROPERTIES = { id: { type: 'string', description: 'State ID' }, name: { type: 'string', description: 'State name (e.g., "Todo", "In Progress")' }, - type: { type: 'string', description: 'State type (unstarted, started, completed, canceled)' }, + description: { type: 'string', description: 'State description' }, + type: { + type: 'string', + description: 'State type (triage, backlog, unstarted, started, completed, canceled)', + }, color: { type: 'string', description: 'State color (hex)' }, position: { type: 'number', description: 'State position in workflow' }, + createdAt: { type: 'string', description: 'Creation timestamp (ISO 8601)' }, + updatedAt: { type: 'string', description: 'Last update timestamp (ISO 8601)' }, + archivedAt: { type: 'string', description: 'Archive timestamp (ISO 8601)' }, team: TEAM_OUTPUT, } as const satisfies Record @@ -343,8 +355,12 @@ export const CUSTOMER_OUTPUT_PROPERTIES = { items: { type: 'string', description: 'External ID' }, }, logoUrl: { type: 'string', description: 'Logo URL' }, + slugId: { type: 'string', description: 'Unique URL slug' }, approximateNeedCount: { type: 'number', description: 'Number of customer needs' }, + revenue: { type: 'number', description: 'Annual revenue' }, + size: { type: 'number', description: 'Organization size' }, createdAt: { type: 'string', description: 'Creation timestamp (ISO 8601)' }, + updatedAt: { type: 'string', description: 'Last update timestamp (ISO 8601)' }, archivedAt: { type: 'string', description: 'Archive timestamp (ISO 8601)' }, } as const satisfies Record @@ -378,11 +394,12 @@ export const CUSTOMER_NEED_OUTPUT_PROPERTIES = { export const CUSTOMER_STATUS_OUTPUT_PROPERTIES = { id: { type: 'string', description: 'Customer status ID' }, name: { type: 'string', description: 'Status name' }, - displayName: { type: 'string', description: 'Display name' }, description: { type: 'string', description: 'Status description' }, color: { type: 'string', description: 'Status color (hex)' }, position: { type: 'number', description: 'Position in list' }, + type: { type: 'string', description: 'Status type (active, inactive)' }, createdAt: { type: 'string', description: 'Creation timestamp (ISO 8601)' }, + updatedAt: { type: 'string', description: 'Last updated timestamp (ISO 8601)' }, archivedAt: { type: 'string', description: 'Archive timestamp (ISO 8601)' }, } as const satisfies Record @@ -410,6 +427,7 @@ export const PROJECT_LABEL_OUTPUT_PROPERTIES = { color: { type: 'string', description: 'Label color (hex)' }, isGroup: { type: 'boolean', description: 'Whether this label is a group' }, createdAt: { type: 'string', description: 'Creation timestamp (ISO 8601)' }, + updatedAt: { type: 'string', description: 'Last update timestamp (ISO 8601)' }, archivedAt: { type: 'string', description: 'Archive timestamp (ISO 8601)' }, } as const satisfies Record @@ -422,6 +440,9 @@ export const PROJECT_MILESTONE_OUTPUT_PROPERTIES = { description: { type: 'string', description: 'Milestone description' }, projectId: { type: 'string', description: 'Project ID' }, targetDate: { type: 'string', description: 'Target date (YYYY-MM-DD)' }, + progress: { type: 'number', description: 'Progress percentage (0-1)' }, + sortOrder: { type: 'number', description: 'Sort order within the project' }, + status: { type: 'string', description: 'Milestone status (done, next, overdue, unstarted)' }, createdAt: { type: 'string', description: 'Creation timestamp (ISO 8601)' }, archivedAt: { type: 'string', description: 'Archive timestamp (ISO 8601)' }, } as const satisfies Record @@ -444,7 +465,12 @@ export const PROJECT_STATUS_OUTPUT_PROPERTIES = { color: { type: 'string', description: 'Status color (hex)' }, indefinite: { type: 'boolean', description: 'Whether this status is indefinite' }, position: { type: 'number', description: 'Position in list' }, + type: { + type: 'string', + description: 'Status type (backlog, planned, started, paused, completed, canceled)', + }, createdAt: { type: 'string', description: 'Creation timestamp (ISO 8601)' }, + updatedAt: { type: 'string', description: 'Last updated timestamp (ISO 8601)' }, archivedAt: { type: 'string', description: 'Archive timestamp (ISO 8601)' }, } as const satisfies Record @@ -587,6 +613,10 @@ export interface LinearLabel { name: string color: string description?: string + isGroup: boolean + createdAt: string + updatedAt: string + archivedAt?: string team?: { id: string name: string @@ -596,9 +626,13 @@ export interface LinearLabel { export interface LinearWorkflowState { id: string name: string + description?: string type: string color: string position: number + createdAt: string + updatedAt: string + archivedAt?: string team: { id: string name: string @@ -613,6 +647,7 @@ export interface LinearCycle { endsAt: string completedAt?: string progress: number + createdAt: string team: { id: string name: string @@ -710,6 +745,7 @@ export interface LinearSearchIssuesParams { teamId?: string includeArchived?: boolean first?: number + after?: string accessToken?: string } @@ -1205,7 +1241,7 @@ export interface LinearAttachment { subtitle?: string url: string createdAt: string - updatedAt?: string + updatedAt: string } export interface LinearCreateAttachmentResponse extends ToolResponse { @@ -1366,8 +1402,12 @@ export interface LinearCustomer { domains: string[] externalIds: string[] logoUrl?: string + slugId: string approximateNeedCount: number + revenue?: number + size?: number createdAt: string + updatedAt: string archivedAt?: string } @@ -1542,11 +1582,12 @@ export interface LinearMergeCustomersResponse extends ToolResponse { export interface LinearCustomerStatus { id: string name: string - displayName: string description?: string color: string position: number + type: string createdAt: string + updatedAt: string archivedAt?: string } @@ -1593,12 +1634,18 @@ export interface LinearDeleteCustomerStatusResponse extends ToolResponse { } export interface LinearListCustomerStatusesParams { + first?: number + after?: string accessToken?: string } export interface LinearListCustomerStatusesResponse extends ToolResponse { output: { customerStatuses?: LinearCustomerStatus[] + pageInfo?: { + hasNextPage: boolean + endCursor?: string + } } } @@ -1658,12 +1705,18 @@ export interface LinearDeleteCustomerTierResponse extends ToolResponse { } export interface LinearListCustomerTiersParams { + first?: number + after?: string accessToken?: string } export interface LinearListCustomerTiersResponse extends ToolResponse { output: { customerTiers?: LinearCustomerTier[] + pageInfo?: { + hasNextPage: boolean + endCursor?: string + } } } @@ -1676,6 +1729,7 @@ export interface LinearProjectLabel { color?: string isGroup: boolean createdAt: string + updatedAt: string archivedAt?: string } @@ -1720,13 +1774,19 @@ export interface LinearDeleteProjectLabelResponse extends ToolResponse { } export interface LinearListProjectLabelsParams { - accessToken?: string projectId?: string + first?: number + after?: string + accessToken?: string } export interface LinearListProjectLabelsResponse extends ToolResponse { output: { projectLabels?: LinearProjectLabel[] + pageInfo?: { + hasNextPage: boolean + endCursor?: string + } } } @@ -1764,6 +1824,9 @@ export interface LinearProjectMilestone { description?: string projectId: string targetDate?: string + progress: number + sortOrder: number + status: string createdAt: string archivedAt?: string } @@ -1809,12 +1872,18 @@ export interface LinearDeleteProjectMilestoneResponse extends ToolResponse { export interface LinearListProjectMilestonesParams { projectId: string + first?: number + after?: string accessToken?: string } export interface LinearListProjectMilestonesResponse extends ToolResponse { output: { projectMilestones?: LinearProjectMilestone[] + pageInfo?: { + hasNextPage: boolean + endCursor?: string + } } } @@ -1827,7 +1896,9 @@ export interface LinearProjectStatus { color: string indefinite: boolean position: number + type: string createdAt: string + updatedAt: string archivedAt?: string } @@ -1875,12 +1946,18 @@ export interface LinearDeleteProjectStatusResponse extends ToolResponse { } export interface LinearListProjectStatusesParams { + first?: number + after?: string accessToken?: string } export interface LinearListProjectStatusesResponse extends ToolResponse { output: { projectStatuses?: LinearProjectStatus[] + pageInfo?: { + hasNextPage: boolean + endCursor?: string + } } } diff --git a/apps/sim/tools/linear/update_attachment.ts b/apps/sim/tools/linear/update_attachment.ts index 7fcddeeb7..4afbe56bb 100644 --- a/apps/sim/tools/linear/update_attachment.ts +++ b/apps/sim/tools/linear/update_attachment.ts @@ -71,6 +71,7 @@ export const linearUpdateAttachmentTool: ToolConfig< title subtitle url + createdAt updatedAt } } diff --git a/apps/sim/tools/linear/update_customer.ts b/apps/sim/tools/linear/update_customer.ts index cc2ee8ffd..e635d5f29 100644 --- a/apps/sim/tools/linear/update_customer.ts +++ b/apps/sim/tools/linear/update_customer.ts @@ -137,8 +137,12 @@ export const linearUpdateCustomerTool: ToolConfig< domains externalIds logoUrl + slugId approximateNeedCount + revenue + size createdAt + updatedAt archivedAt } } diff --git a/apps/sim/tools/linear/update_customer_status.ts b/apps/sim/tools/linear/update_customer_status.ts index 1fbcb58a7..290ceedd3 100644 --- a/apps/sim/tools/linear/update_customer_status.ts +++ b/apps/sim/tools/linear/update_customer_status.ts @@ -2,6 +2,7 @@ import type { LinearUpdateCustomerStatusParams, LinearUpdateCustomerStatusResponse, } from '@/tools/linear/types' +import { CUSTOMER_STATUS_OUTPUT_PROPERTIES } from '@/tools/linear/types' import type { ToolConfig } from '@/tools/types' export const linearUpdateCustomerStatusTool: ToolConfig< @@ -37,18 +38,18 @@ export const linearUpdateCustomerStatusTool: ToolConfig< visibility: 'user-or-llm', description: 'Updated status color', }, - displayName: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'Updated display name', - }, description: { type: 'string', required: false, visibility: 'user-or-llm', description: 'Updated description', }, + displayName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Updated display name', + }, position: { type: 'number', required: false, @@ -78,12 +79,12 @@ export const linearUpdateCustomerStatusTool: ToolConfig< if (params.color != null && params.color !== '') { input.color = params.color } - if (params.displayName != null && params.displayName !== '') { - input.displayName = params.displayName - } if (params.description != null && params.description !== '') { input.description = params.description } + if (params.displayName != null && params.displayName !== '') { + input.displayName = params.displayName + } if (params.position != null) { input.position = params.position } @@ -96,11 +97,12 @@ export const linearUpdateCustomerStatusTool: ToolConfig< customerStatus { id name - displayName description color position + type createdAt + updatedAt archivedAt } } @@ -138,6 +140,7 @@ export const linearUpdateCustomerStatusTool: ToolConfig< customerStatus: { type: 'object', description: 'The updated customer status', + properties: CUSTOMER_STATUS_OUTPUT_PROPERTIES, }, }, } diff --git a/apps/sim/tools/linear/update_label.ts b/apps/sim/tools/linear/update_label.ts index 42c548892..ea34b0882 100644 --- a/apps/sim/tools/linear/update_label.ts +++ b/apps/sim/tools/linear/update_label.ts @@ -71,6 +71,10 @@ export const linearUpdateLabelTool: ToolConfig Date: Fri, 6 Feb 2026 00:14:43 -0800 Subject: [PATCH 3/4] fix(resolver): response format and evaluator metrics in deactivated branch (#3152) * fix(resolver): response format in deactivated branch * add evaluator metrics too * add child workflow id to the workflow block outputs * cleanup typing --- apps/sim/blocks/blocks/workflow.ts | 1 + apps/sim/blocks/blocks/workflow_input.ts | 1 + apps/sim/executor/utils/block-data.ts | 56 +++++++++++++++++++++--- 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/apps/sim/blocks/blocks/workflow.ts b/apps/sim/blocks/blocks/workflow.ts index d30ab4c6d..37d0826aa 100644 --- a/apps/sim/blocks/blocks/workflow.ts +++ b/apps/sim/blocks/blocks/workflow.ts @@ -42,6 +42,7 @@ export const WorkflowBlock: BlockConfig = { outputs: { success: { type: 'boolean', description: 'Execution success status' }, childWorkflowName: { type: 'string', description: 'Child workflow name' }, + childWorkflowId: { type: 'string', description: 'Child workflow ID' }, result: { type: 'json', description: 'Workflow execution result' }, error: { type: 'string', description: 'Error message' }, childTraceSpans: { diff --git a/apps/sim/blocks/blocks/workflow_input.ts b/apps/sim/blocks/blocks/workflow_input.ts index 24c3b3f67..febed399c 100644 --- a/apps/sim/blocks/blocks/workflow_input.ts +++ b/apps/sim/blocks/blocks/workflow_input.ts @@ -41,6 +41,7 @@ export const WorkflowInputBlock: BlockConfig = { outputs: { success: { type: 'boolean', description: 'Execution success status' }, childWorkflowName: { type: 'string', description: 'Child workflow name' }, + childWorkflowId: { type: 'string', description: 'Child workflow ID' }, result: { type: 'json', description: 'Workflow execution result' }, error: { type: 'string', description: 'Error message' }, childTraceSpans: { diff --git a/apps/sim/executor/utils/block-data.ts b/apps/sim/executor/utils/block-data.ts index 9875c79e9..a5ef28d99 100644 --- a/apps/sim/executor/utils/block-data.ts +++ b/apps/sim/executor/utils/block-data.ts @@ -1,3 +1,7 @@ +import { + extractFieldsFromSchema, + parseResponseFormatSafely, +} from '@/lib/core/utils/response-format' import { normalizeInputFormatValue } from '@/lib/workflows/input-format' import { isTriggerBehavior, normalizeName } from '@/executor/constants' import type { ExecutionContext } from '@/executor/types' @@ -43,23 +47,53 @@ function getInputFormatFields(block: SerializedBlock): OutputSchema { const schema: OutputSchema = {} for (const field of inputFormat) { if (!field.name) continue - schema[field.name] = { - type: (field.type || 'any') as 'string' | 'number' | 'boolean' | 'object' | 'array' | 'any', - } + schema[field.name] = { type: field.type || 'any' } } return schema } +function getEvaluatorMetricsSchema(block: SerializedBlock): OutputSchema | undefined { + if (block.metadata?.id !== 'evaluator') return undefined + + const metrics = block.config?.params?.metrics + if (!Array.isArray(metrics) || metrics.length === 0) return undefined + + const validMetrics = metrics.filter( + (m: { name?: string }) => m?.name && typeof m.name === 'string' + ) + if (validMetrics.length === 0) return undefined + + const schema: OutputSchema = { ...(block.outputs as OutputSchema) } + for (const metric of validMetrics) { + schema[metric.name.toLowerCase()] = { type: 'number' } + } + return schema +} + +function getResponseFormatSchema(block: SerializedBlock): OutputSchema | undefined { + const responseFormatValue = block.config?.params?.responseFormat + if (!responseFormatValue) return undefined + + const parsed = parseResponseFormatSafely(responseFormatValue, block.id) + if (!parsed) return undefined + + const fields = extractFieldsFromSchema(parsed) + if (fields.length === 0) return undefined + + const schema: OutputSchema = {} + for (const field of fields) { + schema[field.name] = { type: field.type || 'any' } + } + return schema +} + export function getBlockSchema( block: SerializedBlock, toolConfig?: ToolConfig ): OutputSchema | undefined { const blockType = block.metadata?.id - // For blocks that expose inputFormat as outputs, always merge them - // This includes both triggers (start_trigger, generic_webhook) and - // non-triggers (starter, human_in_the_loop) that have inputFormat if ( blockType && BLOCKS_WITH_INPUT_FORMAT_OUTPUTS.includes( @@ -74,6 +108,16 @@ export function getBlockSchema( } } + const evaluatorSchema = getEvaluatorMetricsSchema(block) + if (evaluatorSchema) { + return evaluatorSchema + } + + const responseFormatSchema = getResponseFormatSchema(block) + if (responseFormatSchema) { + return responseFormatSchema + } + const isTrigger = isTriggerBehavior(block) if (isTrigger && block.outputs && Object.keys(block.outputs).length > 0) { From ed5ed97c07aa95f4bf961a26155cae669bda36c0 Mon Sep 17 00:00:00 2001 From: Waleed Date: Fri, 6 Feb 2026 00:27:17 -0800 Subject: [PATCH 4/4] feat(slack): add file attachment support to slack webhook trigger (#3151) * feat(slack): add file attachment support to slack webhook trigger * additional file handling * lint * ack comment --- apps/sim/background/webhook-execution.ts | 30 ++++- apps/sim/lib/webhooks/utils.server.ts | 164 +++++++++++++++++++---- apps/sim/triggers/slack/webhook.ts | 33 ++++- 3 files changed, 196 insertions(+), 31 deletions(-) diff --git a/apps/sim/background/webhook-execution.ts b/apps/sim/background/webhook-execution.ts index c8abb1b39..fa7ce1bdf 100644 --- a/apps/sim/background/webhook-execution.ts +++ b/apps/sim/background/webhook-execution.ts @@ -21,6 +21,7 @@ import { executeWorkflowCore } from '@/lib/workflows/executor/execution-core' import { PauseResumeManager } from '@/lib/workflows/executor/human-in-the-loop-manager' import { loadDeployedWorkflowState } from '@/lib/workflows/persistence/utils' import { getWorkflowById } from '@/lib/workflows/utils' +import { getBlock } from '@/blocks' import { ExecutionSnapshot } from '@/executor/execution/snapshot' import type { ExecutionMetadata } from '@/executor/execution/types' import { hasExecutionResult } from '@/executor/utils/errors' @@ -74,8 +75,21 @@ async function processTriggerFileOutputs( logger.error(`[${context.requestId}] Error processing ${currentPath}:`, error) processed[key] = val } + } else if ( + outputDef && + typeof outputDef === 'object' && + (outputDef.type === 'object' || outputDef.type === 'json') && + outputDef.properties + ) { + // Explicit object schema with properties - recurse into properties + processed[key] = await processTriggerFileOutputs( + val, + outputDef.properties, + context, + currentPath + ) } else if (outputDef && typeof outputDef === 'object' && !outputDef.type) { - // Nested object in schema - recurse with the nested schema + // Nested object in schema (flat pattern) - recurse with the nested schema processed[key] = await processTriggerFileOutputs(val, outputDef, context, currentPath) } else { // Not a file output - keep as is @@ -405,11 +419,23 @@ async function executeWebhookJobInternal( const rawSelectedTriggerId = triggerBlock?.subBlocks?.selectedTriggerId?.value const rawTriggerId = triggerBlock?.subBlocks?.triggerId?.value - const resolvedTriggerId = [rawSelectedTriggerId, rawTriggerId].find( + let resolvedTriggerId = [rawSelectedTriggerId, rawTriggerId].find( (candidate): candidate is string => typeof candidate === 'string' && isTriggerValid(candidate) ) + if (!resolvedTriggerId) { + const blockConfig = getBlock(triggerBlock.type) + if (blockConfig?.category === 'triggers' && isTriggerValid(triggerBlock.type)) { + resolvedTriggerId = triggerBlock.type + } else if (triggerBlock.triggerMode && blockConfig?.triggers?.enabled) { + const available = blockConfig.triggers?.available?.[0] + if (available && isTriggerValid(available)) { + resolvedTriggerId = available + } + } + } + if (resolvedTriggerId) { const triggerConfig = getTrigger(resolvedTriggerId) diff --git a/apps/sim/lib/webhooks/utils.server.ts b/apps/sim/lib/webhooks/utils.server.ts index 8b99f7dec..39371150c 100644 --- a/apps/sim/lib/webhooks/utils.server.ts +++ b/apps/sim/lib/webhooks/utils.server.ts @@ -527,6 +527,113 @@ export async function validateTwilioSignature( } } +const SLACK_FILE_HOSTS = new Set(['files.slack.com', 'files-pri.slack.com']) +const SLACK_MAX_FILE_SIZE = 50 * 1024 * 1024 // 50 MB +const SLACK_MAX_FILES = 10 + +/** + * Downloads file attachments from Slack using the bot token. + * Returns files in the format expected by WebhookAttachmentProcessor: + * { name, data (base64 string), mimeType, size } + * + * Security: + * - Validates each url_private against allowlisted Slack file hosts + * - Uses validateUrlWithDNS + secureFetchWithPinnedIP to prevent SSRF + * - Enforces per-file size limit and max file count + */ +async function downloadSlackFiles( + rawFiles: any[], + botToken: string +): Promise> { + const filesToProcess = rawFiles.slice(0, SLACK_MAX_FILES) + const downloaded: Array<{ name: string; data: string; mimeType: string; size: number }> = [] + + for (const file of filesToProcess) { + const urlPrivate = file.url_private as string | undefined + if (!urlPrivate) { + continue + } + + // Validate the URL points to a known Slack file host + let parsedUrl: URL + try { + parsedUrl = new URL(urlPrivate) + } catch { + logger.warn('Slack file has invalid url_private, skipping', { fileId: file.id }) + continue + } + + if (!SLACK_FILE_HOSTS.has(parsedUrl.hostname)) { + logger.warn('Slack file url_private points to unexpected host, skipping', { + fileId: file.id, + hostname: sanitizeUrlForLog(urlPrivate), + }) + continue + } + + // Skip files that exceed the size limit + const reportedSize = Number(file.size) || 0 + if (reportedSize > SLACK_MAX_FILE_SIZE) { + logger.warn('Slack file exceeds size limit, skipping', { + fileId: file.id, + size: reportedSize, + limit: SLACK_MAX_FILE_SIZE, + }) + continue + } + + try { + const urlValidation = await validateUrlWithDNS(urlPrivate, 'url_private') + if (!urlValidation.isValid) { + logger.warn('Slack file url_private failed DNS validation, skipping', { + fileId: file.id, + error: urlValidation.error, + }) + continue + } + + const response = await secureFetchWithPinnedIP(urlPrivate, urlValidation.resolvedIP!, { + headers: { Authorization: `Bearer ${botToken}` }, + }) + + if (!response.ok) { + logger.warn('Failed to download Slack file, skipping', { + fileId: file.id, + status: response.status, + }) + continue + } + + const arrayBuffer = await response.arrayBuffer() + const buffer = Buffer.from(arrayBuffer) + + // Verify the actual downloaded size doesn't exceed our limit + if (buffer.length > SLACK_MAX_FILE_SIZE) { + logger.warn('Downloaded Slack file exceeds size limit, skipping', { + fileId: file.id, + actualSize: buffer.length, + limit: SLACK_MAX_FILE_SIZE, + }) + continue + } + + downloaded.push({ + name: file.name || 'download', + data: buffer.toString('base64'), + mimeType: file.mimetype || 'application/octet-stream', + size: buffer.length, + }) + } catch (error) { + logger.error('Error downloading Slack file, skipping', { + fileId: file.id, + error: error instanceof Error ? error.message : String(error), + }) + } + } + + return downloaded +} + /** * Format webhook input based on provider */ @@ -787,43 +894,44 @@ export async function formatWebhookInput( } if (foundWebhook.provider === 'slack') { - const event = body?.event + const providerConfig = (foundWebhook.providerConfig as Record) || {} + const botToken = providerConfig.botToken as string | undefined + const includeFiles = Boolean(providerConfig.includeFiles) - if (event && body?.type === 'event_callback') { - return { - event: { - event_type: event.type || '', - channel: event.channel || '', - channel_name: '', - user: event.user || '', - user_name: '', - text: event.text || '', - timestamp: event.ts || event.event_ts || '', - thread_ts: event.thread_ts || '', - team_id: body.team_id || event.team || '', - event_id: body.event_id || '', - }, - } + const rawEvent = body?.event + + if (!rawEvent) { + logger.warn('Unknown Slack event type', { + type: body?.type, + hasEvent: false, + bodyKeys: Object.keys(body || {}), + }) } - logger.warn('Unknown Slack event type', { - type: body?.type, - hasEvent: !!body?.event, - bodyKeys: Object.keys(body || {}), - }) + const rawFiles: any[] = rawEvent?.files ?? [] + const hasFiles = rawFiles.length > 0 + + let files: any[] = [] + if (hasFiles && includeFiles && botToken) { + files = await downloadSlackFiles(rawFiles, botToken) + } else if (hasFiles && includeFiles && !botToken) { + logger.warn('Slack message has files and includeFiles is enabled, but no bot token provided') + } return { event: { - event_type: body?.event?.type || body?.type || 'unknown', - channel: body?.event?.channel || '', + event_type: rawEvent?.type || body?.type || 'unknown', + channel: rawEvent?.channel || '', channel_name: '', - user: body?.event?.user || '', + user: rawEvent?.user || '', user_name: '', - text: body?.event?.text || '', - timestamp: body?.event?.ts || '', - thread_ts: body?.event?.thread_ts || '', - team_id: body?.team_id || '', + text: rawEvent?.text || '', + timestamp: rawEvent?.ts || rawEvent?.event_ts || '', + thread_ts: rawEvent?.thread_ts || '', + team_id: body?.team_id || rawEvent?.team || '', event_id: body?.event_id || '', + hasFiles, + files, }, } } diff --git a/apps/sim/triggers/slack/webhook.ts b/apps/sim/triggers/slack/webhook.ts index 4c9bd8990..3d22e3be2 100644 --- a/apps/sim/triggers/slack/webhook.ts +++ b/apps/sim/triggers/slack/webhook.ts @@ -30,6 +30,27 @@ export const slackWebhookTrigger: TriggerConfig = { required: true, mode: 'trigger', }, + { + id: 'botToken', + title: 'Bot Token', + type: 'short-input', + placeholder: 'xoxb-...', + description: + 'The bot token from your Slack app. Required for downloading files attached to messages.', + password: true, + required: false, + mode: 'trigger', + }, + { + id: 'includeFiles', + title: 'Include File Attachments', + type: 'switch', + defaultValue: false, + description: + 'Download and include file attachments from messages. Requires a bot token with files:read scope.', + required: false, + mode: 'trigger', + }, { id: 'triggerSave', title: '', @@ -46,9 +67,10 @@ export const slackWebhookTrigger: TriggerConfig = { 'Go to Slack Apps page', 'If you don\'t have an app:
  • Create an app from scratch
  • Give it a name and select your workspace
', 'Go to "Basic Information", find the "Signing Secret", and paste it in the field above.', - 'Go to "OAuth & Permissions" and add bot token scopes:
  • app_mentions:read - For viewing messages that tag your bot with an @
  • chat:write - To send messages to channels your bot is a part of
', + 'Go to "OAuth & Permissions" and add bot token scopes:
  • app_mentions:read - For viewing messages that tag your bot with an @
  • chat:write - To send messages to channels your bot is a part of
  • files:read - To access files and images shared in messages
', 'Go to "Event Subscriptions":
  • Enable events
  • Under "Subscribe to Bot Events", add app_mention to listen to messages that mention your bot
  • Paste the Webhook URL above into the "Request URL" field
', 'Go to "Install App" in the left sidebar and install the app into your desired Slack workspace and channel.', + 'Copy the "Bot User OAuth Token" (starts with xoxb-) and paste it in the Bot Token field above to enable file downloads.', 'Save changes in both Slack and here.', ] .map( @@ -106,6 +128,15 @@ export const slackWebhookTrigger: TriggerConfig = { type: 'string', description: 'Unique event identifier', }, + hasFiles: { + type: 'boolean', + description: 'Whether the message has file attachments', + }, + files: { + type: 'file[]', + description: + 'File attachments downloaded from the message (if includeFiles is enabled and bot token is provided)', + }, }, }, },