Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
function mapTask(task: ServerWorkflowTask): StepDefinition {
// executionType is passed through as-is; each schema's .default().catch() handles
// missing or unsupported values without requiring an explicit mapping here.
const base = { prompt: task.prompt, executionType: task.executionType };
const base = { prompt: task.prompt, executionType: task.executionType, title: task.title };

switch (task.taskType) {
case ServerTaskTypeEnum.McpServer:
Expand Down Expand Up @@ -65,6 +65,7 @@ function mapCondition(condition: ServerWorkflowCondition): ConditionStepDefiniti
type: StepType.Condition,
prompt: condition.prompt,
executionType: condition.executionType,
title: condition.title,
options,
});
}
Expand Down
22 changes: 14 additions & 8 deletions packages/workflow-executor/src/executors/base-step-executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,16 +224,22 @@ export default abstract class BaseStepExecutor<TStep extends StepDefinition = St
}

protected buildContextMessage(): SystemMessage {
const { user } = this.context;
const { user, stepDefinition } = this.context;
const now = new Date();

return new SystemMessage(
[
`Step executed by: ${user.firstName} ${user.lastName} (${user.email}, id: ${user.id})`,
`Role: ${user.role} | Team: ${user.team}`,
`Current date and time: ${now.toISOString()} (UTC)`,
].join('\n'),
);
const lines = [
`Step executed by: ${user.firstName} ${user.lastName} (${user.email}, id: ${user.id})`,
`Role: ${user.role} | Team: ${user.team}`,
`Current date and time: ${now.toISOString()} (UTC)`,
];

// Fall back to the step title only when there is no prompt — the prompt, when present, is the
// authoritative intent (surfaced by each executor), so the title would just be noise.
if (!stepDefinition.prompt?.trim() && stepDefinition.title) {
lines.push(`Step title: "${stepDefinition.title}"`);
}

return new SystemMessage(lines.join('\n'));
}

protected async buildPreviousStepsMessages(): Promise<SystemMessage[]> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export enum StepExecutionMode {
const sharedFields = {
prompt: z.string().optional(),
aiConfigName: z.string().optional(),
title: z.string().optional(),
};

// Use z.enum(EnumObject), not z.nativeEnum — the latter is deprecated in zod 4.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ describe('toAvailableStepExecution', () => {
stepDefinition: {
type: StepType.ReadRecord,
prompt: 'prompt',
title: 'Task',
executionType: ServerStepExecutionTypeEnum.FullyAutomated,
},
previousSteps: [],
Expand Down Expand Up @@ -189,6 +190,7 @@ describe('toAvailableStepExecution', () => {
expect(result?.stepDefinition).toEqual({
type: StepType.Guidance,
prompt: 'follow the guide',
title: 'guidance',
executionType: ServerStepExecutionTypeEnum.Manual,
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ describe('toStepDefinition', () => {
expect(toStepDefinition(task)).toEqual({
type: StepType.ReadRecord,
prompt: 'read it',
title: 'Test task',
executionType: ServerStepExecutionTypeEnum.FullyAutomated,
});
});
Expand All @@ -70,6 +71,7 @@ describe('toStepDefinition', () => {
expect(toStepDefinition(task)).toEqual({
type: StepType.UpdateRecord,
prompt: 'update it',
title: 'Test task',
executionType: ServerStepExecutionTypeEnum.AutomatedWithConfirmation,
});
});
Expand All @@ -80,6 +82,7 @@ describe('toStepDefinition', () => {
expect(toStepDefinition(task)).toEqual({
type: StepType.TriggerAction,
prompt: 'trigger it',
title: 'Test task',
executionType: ServerStepExecutionTypeEnum.AutomatedWithConfirmation,
});
});
Expand All @@ -93,6 +96,7 @@ describe('toStepDefinition', () => {
expect(toStepDefinition(task)).toEqual({
type: StepType.LoadRelatedRecord,
prompt: 'load it',
title: 'Test task',
executionType: ServerStepExecutionTypeEnum.AutomatedWithConfirmation,
});
});
Expand All @@ -108,6 +112,7 @@ describe('toStepDefinition', () => {
type: StepType.Mcp,
prompt: 'run mcp',
mcpServerId: 'mcp-abc',
title: 'Test task',
executionType: ServerStepExecutionTypeEnum.AutomatedWithConfirmation,
});
});
Expand All @@ -128,6 +133,7 @@ describe('toStepDefinition', () => {
expect(toStepDefinition(task)).toEqual({
type: StepType.Guidance,
prompt: 'guide them',
title: 'Test task',
executionType: StepExecutionMode.Manual,
});
});
Expand Down Expand Up @@ -196,6 +202,7 @@ describe('toStepDefinition', () => {
expect(toStepDefinition(condition)).toEqual({
type: StepType.Condition,
prompt: 'Choose one',
title: 'Test condition',
options: ['Yes', 'No'],
executionType: StepExecutionMode.FullyAutomated,
});
Expand All @@ -210,6 +217,7 @@ describe('toStepDefinition', () => {
expect(toStepDefinition(condition)).toEqual({
type: StepType.Condition,
prompt: 'Choose one',
title: 'Test condition',
options: ['Approve', 'Reject'],
executionType: StepExecutionMode.FullyAutomated,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ class TestableExecutor extends BaseStepExecutor {
return super.buildPreviousStepsMessages();
}

override buildContextMessage(): SystemMessage {
return super.buildContextMessage();
}

override invokeWithTool<T = Record<string, unknown>>(
messages: BaseMessage[],
tool: DynamicStructuredTool,
Expand Down Expand Up @@ -175,6 +179,53 @@ function makeContext(
}

describe('BaseStepExecutor', () => {
describe('buildContextMessage', () => {
it('falls back to the step title when there is no prompt', () => {
const context = makeContext({
stepDefinition: {
type: StepType.Condition,
executionType: StepExecutionMode.Manual,
options: ['A', 'B'],
title: 'Load the store',
},
});

const message = new TestableExecutor(context).buildContextMessage();

expect(message.content as string).toContain('Step title: "Load the store"');
});

it('omits the title when a prompt is present (the prompt is authoritative)', () => {
const context = makeContext({
stepDefinition: {
type: StepType.Condition,
executionType: StepExecutionMode.Manual,
options: ['A', 'B'],
prompt: 'Pick one',
title: 'Load the store',
},
});

const message = new TestableExecutor(context).buildContextMessage();

expect(message.content as string).not.toContain('Step title');
});

it('omits the title line when the step has neither prompt nor title', () => {
const context = makeContext({
stepDefinition: {
type: StepType.Condition,
executionType: StepExecutionMode.Manual,
options: ['A', 'B'],
},
});

const message = new TestableExecutor(context).buildContextMessage();

expect(message.content as string).not.toContain('Step title');
});
});

describe('buildPreviousStepsMessages', () => {
it('returns empty array for empty history', async () => {
const executor = new TestableExecutor(makeContext());
Expand Down
Loading