feat(workflow-tools): add get_logic_function_source tool (#22835)

## 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)

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22835?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Joshua Freedman
2026-07-13 11:41:00 -04:00
committed by GitHub
parent fd7935f343
commit 9f75506896
3 changed files with 120 additions and 0 deletions
@@ -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,
@@ -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');
});
});
@@ -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<WorkflowToolDependencies, 'logicFunctionFromSourceService'>,
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}`,
};
}
},
});