From 9f75506896858c60139f92b996fcc59862ddd9a9 Mon Sep 17 00:00:00 2001 From: Joshua Freedman Date: Mon, 13 Jul 2026 11:41:00 -0400 Subject: [PATCH] feat(workflow-tools): add get_logic_function_source tool (#22835) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What / why The workflow agent tools let an AI **write** a CODE step's logic function (`update_logic_function_source`) and **list** logic functions (`list_logic_function_tools`), but there is no tool to **read** an existing function's source. That's fine for greenfield generation — the agent already has in context whatever code it just wrote. But it's a real gap when editing a function the agent did **not** author: to safely modify an existing CODE step it has to see the current source first, and today the only ways to get it are the frontend (`getLogicFunctionSourceCode` query) or the DB. So the agent is forced to either guess or ask a human to paste the code. This adds a small read tool that closes the loop, mirroring the existing `update_logic_function_source` tool. ## How - New `get_logic_function_source` tool that calls the existing `LogicFunctionFromSourceService.getSourceCode({ id, workspaceId })` — the same service method backing the `getLogicFunctionSourceCode` GraphQL resolver the frontend already uses. No new service logic. - Registered in `workflow-tool.workspace-service.ts` alongside `update_logic_function_source` (the dependency `logicFunctionFromSourceService` is already injected). - Unit test covering the success and error paths, matching the `get-*` tool test convention. ## Notes - Read-only, additive; no schema or API changes. - Naturally pairs with `update_logic_function_source`: read → edit → write. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Review in cubic --- .../workflow-tool.workspace-service.ts | 6 ++ .../get-logic-function-source.tool.spec.ts | 60 +++++++++++++++++++ .../tools/get-logic-function-source.tool.ts | 54 +++++++++++++++++ 3 files changed, 120 insertions(+) create mode 100644 packages/twenty-server/src/modules/workflow/workflow-tools/tools/__tests__/get-logic-function-source.tool.spec.ts create mode 100644 packages/twenty-server/src/modules/workflow/workflow-tools/tools/get-logic-function-source.tool.ts diff --git a/packages/twenty-server/src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service.ts index 365d3f43fd..15904c5058 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service.ts @@ -25,6 +25,7 @@ import { createDeactivateWorkflowVersionTool } from 'src/modules/workflow/workfl import { createDeleteWorkflowTool } from 'src/modules/workflow/workflow-tools/tools/delete-workflow.tool'; import { createDeleteWorkflowVersionEdgeTool } from 'src/modules/workflow/workflow-tools/tools/delete-workflow-version-edge.tool'; import { createDeleteWorkflowVersionStepTool } from 'src/modules/workflow/workflow-tools/tools/delete-workflow-version-step.tool'; +import { createGetLogicFunctionSourceTool } from 'src/modules/workflow/workflow-tools/tools/get-logic-function-source.tool'; import { createGetWorkflowCurrentVersionTool } from 'src/modules/workflow/workflow-tools/tools/get-workflow-current-version.tool'; import { createGetWorkflowRunTool } from 'src/modules/workflow/workflow-tools/tools/get-workflow-run.tool'; import { createListLogicFunctionToolsTool } from 'src/modules/workflow/workflow-tools/tools/list-logic-function-tools.tool'; @@ -147,6 +148,10 @@ export class WorkflowToolWorkspaceService { this.deps, contextWithPermissions, ); + const getLogicFunctionSource = createGetLogicFunctionSourceTool( + this.deps, + context, + ); const updateLogicFunctionSource = createUpdateLogicFunctionSourceTool( this.deps, context, @@ -176,6 +181,7 @@ export class WorkflowToolWorkspaceService { [deleteWorkflow.name]: deleteWorkflow, [getWorkflowRun.name]: getWorkflowRun, [listWorkflowRuns.name]: listWorkflowRuns, + [getLogicFunctionSource.name]: getLogicFunctionSource, [updateLogicFunctionSource.name]: updateLogicFunctionSource, [listLogicFunctionTools.name]: listLogicFunctionTools, [updateAgent.name]: updateAgent, diff --git a/packages/twenty-server/src/modules/workflow/workflow-tools/tools/__tests__/get-logic-function-source.tool.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-tools/tools/__tests__/get-logic-function-source.tool.spec.ts new file mode 100644 index 0000000000..db28563fae --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/workflow-tools/tools/__tests__/get-logic-function-source.tool.spec.ts @@ -0,0 +1,60 @@ +import { + type WorkflowToolContext, + type WorkflowToolDependencies, +} from 'src/modules/workflow/workflow-tools/types/workflow-tool-dependencies.type'; + +import { createGetLogicFunctionSourceTool } from '../get-logic-function-source.tool'; + +const WORKSPACE_ID = '20202020-aaaa-4d02-bf25-6aeccf7ea419'; +const LOGIC_FUNCTION_ID = '20202020-bbbb-4d02-bf25-6aeccf7ea419'; + +const buildDeps = (getSourceCode: jest.Mock) => + ({ + logicFunctionFromSourceService: { getSourceCode }, + }) as unknown as Pick< + WorkflowToolDependencies, + 'logicFunctionFromSourceService' + >; + +const buildContext = () => + ({ workspaceId: WORKSPACE_ID }) as unknown as WorkflowToolContext; + +describe('get_logic_function_source tool', () => { + it('returns the source code from the service', async () => { + const source = 'export const main = async () => ({ ok: true });'; + const getSourceCode = jest.fn().mockResolvedValue(source); + + const tool = createGetLogicFunctionSourceTool( + buildDeps(getSourceCode), + buildContext(), + ); + + const result = await tool.execute({ logicFunctionId: LOGIC_FUNCTION_ID }); + + expect(getSourceCode).toHaveBeenCalledWith({ + id: LOGIC_FUNCTION_ID, + workspaceId: WORKSPACE_ID, + }); + expect(result).toEqual({ + success: true, + logicFunctionId: LOGIC_FUNCTION_ID, + sourceHandlerCode: source, + }); + }); + + it('returns a failure result when the service throws', async () => { + const getSourceCode = jest + .fn() + .mockRejectedValue(new Error('Logic function not found')); + + const tool = createGetLogicFunctionSourceTool( + buildDeps(getSourceCode), + buildContext(), + ); + + const result = await tool.execute({ logicFunctionId: LOGIC_FUNCTION_ID }); + + expect(result.success).toBe(false); + expect(result.error).toBe('Logic function not found'); + }); +}); diff --git a/packages/twenty-server/src/modules/workflow/workflow-tools/tools/get-logic-function-source.tool.ts b/packages/twenty-server/src/modules/workflow/workflow-tools/tools/get-logic-function-source.tool.ts new file mode 100644 index 0000000000..7e2880f53c --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/workflow-tools/tools/get-logic-function-source.tool.ts @@ -0,0 +1,54 @@ +import { z } from 'zod'; + +import { + type WorkflowToolContext, + type WorkflowToolDependencies, +} from 'src/modules/workflow/workflow-tools/types/workflow-tool-dependencies.type'; + +const getLogicFunctionSourceSchema = z.object({ + logicFunctionId: z + .string() + .uuid() + .describe( + 'The ID of the logic function to read (from the code step settings.input.logicFunctionId)', + ), +}); + +export const createGetLogicFunctionSourceTool = ( + deps: Pick, + context: WorkflowToolContext, +) => ({ + name: 'get_logic_function_source' as const, + description: `Read the current TypeScript source code of a logic function used in a workflow CODE step. + +Use this to inspect the code that runs when a CODE step executes — for example, before editing an existing function with update_logic_function_source, so the edit is based on the real current source rather than a guess. + +To find the logicFunctionId, look at the code step's settings.input.logicFunctionId field.`, + inputSchema: getLogicFunctionSourceSchema, + execute: async (parameters: { logicFunctionId: string }) => { + try { + const { logicFunctionId } = parameters; + const { workspaceId } = context; + + const sourceHandlerCode = + await deps.logicFunctionFromSourceService.getSourceCode({ + id: logicFunctionId, + workspaceId, + }); + + return { + success: true, + logicFunctionId, + sourceHandlerCode, + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + + return { + success: false, + error: message, + message: `Failed to read logic function source: ${message}`, + }; + } + }, +});