feat: Add AI Agent workflow action node (#12650)

https://github.com/user-attachments/assets/8593e488-cb00-4fd2-b903-5ba5766e0254

---------

Co-authored-by: Antoine Moreaux <moreaux.antoine@gmail.com>
Co-authored-by: martmull <martmull@hotmail.fr>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Baptiste Devessier <baptiste@devessier.fr>
Co-authored-by: Joseph Chiang <josephj6802@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Guillim <guillim@users.noreply.github.com>
Co-authored-by: Raphaël Bosi <71827178+bosiraphael@users.noreply.github.com>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
Co-authored-by: Marie <51697796+ijreilly@users.noreply.github.com>
Co-authored-by: Naifer <161821705+omarNaifer12@users.noreply.github.com>
Co-authored-by: prastoin <paul@twenty.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@twenty.com>
Co-authored-by: Thomas Trompette <thomas.trompette@sfr.fr>
Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
Co-authored-by: Ajay A Adsule <103304466+AjayAdsule@users.noreply.github.com>
Co-authored-by: bosiraphael <raphael.bosi@gmail.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: Marty <91310557+real-marty@users.noreply.github.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Paul Rastoin <45004772+prastoin@users.noreply.github.com>
Co-authored-by: Weiko <corentin@twenty.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: nitin <142569587+ehconitin@users.noreply.github.com>
This commit is contained in:
Abdul Rahman
2025-06-23 01:12:04 +05:30
committed by GitHub
parent 22e126869c
commit 65df511179
75 changed files with 2268 additions and 30 deletions
@@ -2,6 +2,7 @@ import { WorkflowAction, WorkflowTrigger } from '@/workflow/types/Workflow';
import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
import { WorkflowDiagramRunStatus } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { WorkflowEditActionAiAgent } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowEditActionAiAgent';
import { WorkflowActionServerlessFunction } from '@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowActionServerlessFunction';
import { WorkflowEditActionCreateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionCreateRecord';
import { WorkflowEditActionDeleteRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionDeleteRecord';
@@ -188,6 +189,17 @@ export const WorkflowRunStepNodeDetail = ({
/>
);
}
case 'AI_AGENT': {
return (
<WorkflowEditActionAiAgent
key={stepId}
action={stepDefinition.definition}
actionOptions={{
readonly: true,
}}
/>
);
}
}
}
}
@@ -1,6 +1,7 @@
import { WorkflowAction, WorkflowTrigger } from '@/workflow/types/Workflow';
import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
import { WorkflowEditActionAiAgent } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowEditActionAiAgent';
import { WorkflowActionServerlessFunction } from '@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowActionServerlessFunction';
import { WorkflowEditActionCreateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionCreateRecord';
import { WorkflowEditActionDeleteRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionDeleteRecord';
@@ -88,12 +89,12 @@ export const WorkflowStepDetail = ({
/>
);
}
default:
return assertUnreachable(
stepDefinition.definition,
`Expected the step to have an handler; ${JSON.stringify(stepDefinition)}`,
);
}
return assertUnreachable(
stepDefinition.definition,
`Expected the step to have an handler; ${JSON.stringify(stepDefinition)}`,
);
}
case 'action': {
switch (stepDefinition.definition.type) {
@@ -174,12 +175,22 @@ export const WorkflowStepDetail = ({
/>
);
}
case 'AI_AGENT': {
return (
<WorkflowEditActionAiAgent
key={stepId}
action={stepDefinition.definition}
actionOptions={props}
/>
);
}
default:
return assertUnreachable(
stepDefinition.definition,
`Expected the step to have an handler; ${JSON.stringify(stepDefinition)}`,
);
}
}
}
return assertUnreachable(
stepDefinition,
`Expected the step to have an handler; ${JSON.stringify(stepDefinition)}`,
);
};
@@ -0,0 +1,118 @@
import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput';
import { Select } from '@/ui/input/components/Select';
import { WorkflowAiAgentAction } from '@/workflow/types/Workflow';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
import { BaseOutputSchema } from '@/workflow/workflow-variables/types/StepOutputSchema';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { useIcons } from 'twenty-ui/display';
import { RightDrawerSkeletonLoader } from '~/loading/components/RightDrawerSkeletonLoader';
import { useAgentUpdateFormState } from '../hooks/useAgentUpdateFormState';
import { useAiAgentOutputSchema } from '../hooks/useAiAgentOutputSchema';
import { useAiModelOptions } from '../hooks/useAiModelOptions';
import { WorkflowOutputSchemaBuilder } from './WorkflowOutputSchemaBuilder';
const StyledErrorMessage = styled.div`
color: ${({ theme }) => theme.font.color.danger};
font-size: ${({ theme }) => theme.font.size.sm};
font-weight: ${({ theme }) => theme.font.weight.regular};
margin-top: ${({ theme }) => theme.spacing(1)};
`;
type WorkflowEditActionAiAgentProps = {
action: WorkflowAiAgentAction;
actionOptions:
| { readonly: true }
| {
readonly?: false;
onActionUpdate: (action: WorkflowAiAgentAction) => void;
};
};
export const WorkflowEditActionAiAgent = ({
action,
actionOptions,
}: WorkflowEditActionAiAgentProps) => {
const { getIcon } = useIcons();
const { headerTitle, headerIcon, headerIconColor, headerType } =
useWorkflowActionHeader({
action,
defaultTitle: 'AI Agent',
});
const { formValues, handleFieldChange, loading } = useAgentUpdateFormState({
agentId: action.settings.input.agentId,
readonly: actionOptions.readonly === true,
});
const { handleOutputSchemaChange, outputFields } = useAiAgentOutputSchema(
action.settings.outputSchema as BaseOutputSchema,
actionOptions.readonly === true ? undefined : actionOptions.onActionUpdate,
action,
actionOptions.readonly,
);
const modelOptions = useAiModelOptions();
const noModelsAvailable = modelOptions.length === 0;
return loading ? (
<RightDrawerSkeletonLoader />
) : (
<>
<WorkflowStepHeader
onTitleChange={(newName: string) => {
if (actionOptions.readonly === true) {
return;
}
actionOptions.onActionUpdate?.({ ...action, name: newName });
}}
Icon={getIcon(headerIcon)}
iconColor={headerIconColor}
initialTitle={headerTitle}
headerType={headerType}
disabled={actionOptions.readonly}
/>
<WorkflowStepBody>
<div>
<Select
dropdownId="select-model"
label={t`AI Model`}
options={modelOptions}
value={formValues.modelId}
onChange={(value) => handleFieldChange('modelId', value)}
disabled={actionOptions.readonly || noModelsAvailable}
emptyOption={{
label: t`No AI models available`,
value: '',
}}
/>
{noModelsAvailable && (
<StyledErrorMessage>
{t`You haven't configured any model provider. Please set up an API Key in your instance's admin panel or as an environment variable.`}
</StyledErrorMessage>
)}
</div>
<FormTextFieldInput
key={`prompt-${formValues.modelId ? action.id : 'empty'}`}
label={t`Instructions for AI`}
placeholder={t`Describe what you want the AI to do...`}
readonly={actionOptions.readonly}
defaultValue={formValues.prompt}
onChange={(value) => handleFieldChange('prompt', value)}
VariablePicker={WorkflowVariablePicker}
multiline
/>
<WorkflowOutputSchemaBuilder
fields={outputFields}
onChange={handleOutputSchemaChange}
readonly={actionOptions.readonly}
/>
</WorkflowStepBody>
</>
);
};
@@ -0,0 +1,32 @@
import { Select } from '@/ui/input/components/Select';
import { InputSchemaPropertyType } from '@/workflow/types/InputSchema';
import { t } from '@lingui/core/macro';
import { OUTPUT_FIELD_TYPE_OPTIONS } from '../constants/output-field-type-options';
type WorkflowOutputFieldTypeSelectorProps = {
value?: InputSchemaPropertyType;
onChange: (value: InputSchemaPropertyType) => void;
disabled?: boolean;
dropdownId: string;
};
export const WorkflowOutputFieldTypeSelector = ({
value,
onChange,
disabled,
dropdownId,
}: WorkflowOutputFieldTypeSelectorProps) => {
return (
<Select
dropdownId={dropdownId}
label="Field Type"
options={OUTPUT_FIELD_TYPE_OPTIONS.map((option) => ({
...option,
label: t(option.label),
}))}
value={value}
onChange={onChange}
disabled={disabled}
/>
);
};
@@ -0,0 +1,221 @@
import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput';
import { InputLabel } from '@/ui/input/components/InputLabel';
import { InputSchemaPropertyType } from '@/workflow/types/InputSchema';
import { OutputSchemaField } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/constants/output-field-type-options';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { IconPlus, IconTrash } from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
import { v4 } from 'uuid';
import { WorkflowOutputFieldTypeSelector } from './WorkflowOutputFieldTypeSelector';
type WorkflowOutputSchemaBuilderProps = {
fields: OutputSchemaField[];
onChange: (fields: OutputSchemaField[]) => void;
readonly?: boolean;
};
const StyledOutputSchemaContainer = styled.div`
display: flex;
flex-direction: column;
`;
const StyledFieldsContainer = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(2)};
`;
const StyledOutputSchemaFieldContainer = styled.div`
background-color: ${({ theme }) => theme.background.transparent.lighter};
border: 1px solid ${({ theme }) => theme.border.color.light};
border-radius: ${({ theme }) => theme.border.radius.md};
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(1)};
`;
const StyledSettingsContent = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(2)};
padding: ${({ theme }) => theme.spacing(3)};
`;
const StyledSettingsHeader = styled.div`
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
display: grid;
gap: ${({ theme }) => theme.spacing(1)};
padding-right: ${({ theme }) => theme.spacing(2)};
padding-left: ${({ theme }) => theme.spacing(3)};
grid-template-columns: 1fr 24px;
padding-bottom: ${({ theme }) => theme.spacing(2)};
`;
const StyledTitleContainer = styled.div`
color: ${({ theme }) => theme.font.color.primary};
display: flex;
flex-direction: row;
gap: ${({ theme }) => theme.spacing(1)};
padding-top: ${({ theme }) => theme.spacing(3)};
`;
const StyledCloseButtonContainer = styled.div`
padding-top: ${({ theme }) => theme.spacing(2)};
`;
const StyledAddFieldButton = styled.button`
align-items: center;
border: 1px solid ${({ theme }) => theme.border.color.light};
border-radius: ${({ theme }) => theme.border.radius.sm};
color: ${({ theme }) => theme.font.color.secondary};
cursor: pointer;
display: flex;
font-family: inherit;
font-weight: ${({ theme }) => theme.font.weight.medium};
gap: ${({ theme }) => theme.spacing(1)};
justify-content: center;
margin-top: ${({ theme }) => theme.spacing(2)};
padding: ${({ theme }) => theme.spacing(2)};
width: 100%;
background-color: ${({ theme }) => theme.background.transparent.lighter};
&:hover {
background-color: ${({ theme }) => theme.background.transparent.light};
}
`;
const StyledMessageContentContainer = styled.div`
flex-direction: column;
color: ${({ theme }) => theme.font.color.secondary};
display: flex;
gap: ${({ theme }) => theme.spacing(4)};
padding: ${({ theme }) => theme.spacing(4)};
line-height: normal;
`;
const StyledMessageDescription = styled.div`
color: ${({ theme }) => theme.font.color.secondary};
font-weight: ${({ theme }) => theme.font.weight.regular};
`;
export const WorkflowOutputSchemaBuilder = ({
fields,
onChange,
readonly,
}: WorkflowOutputSchemaBuilderProps) => {
const theme = useTheme();
const addField = () => {
const newField: OutputSchemaField = {
id: v4(),
name: '',
description: '',
type: 'TEXT' as InputSchemaPropertyType,
};
onChange([...fields, newField]);
};
const removeField = (id: string) => {
onChange(fields.filter((field) => field.id !== id));
};
const updateField = (id: string, updates: Partial<OutputSchemaField>) => {
onChange(
fields.map((field) =>
field.id === id ? { ...field, ...updates } : field,
),
);
};
return (
<StyledOutputSchemaContainer>
<InputLabel>{t`AI Response Schema`}</InputLabel>
{fields.length === 0 && (
<StyledOutputSchemaFieldContainer>
<StyledMessageContentContainer>
<StyledMessageDescription data-testid="empty-output-schema-message-description">
{t`Click on "Add Output Field" below to define the structure of your AI agent's response. These fields will be used to format and validate the AI's output when the workflow is executed, and can be referenced by subsequent workflow steps.`}
</StyledMessageDescription>
</StyledMessageContentContainer>
</StyledOutputSchemaFieldContainer>
)}
{fields.length > 0 && (
<StyledFieldsContainer>
{fields.map((field, index) => {
const fieldNumber = index + 1;
return (
<StyledOutputSchemaFieldContainer key={field.id}>
<StyledSettingsHeader>
<StyledTitleContainer>
<span>{t`Output Field ${fieldNumber}`}</span>
</StyledTitleContainer>
<StyledCloseButtonContainer>
{!readonly && (
<LightIconButton
testId="close-button"
Icon={IconTrash}
size="small"
accent="secondary"
onClick={() => removeField(field.id)}
/>
)}
</StyledCloseButtonContainer>
</StyledSettingsHeader>
<StyledSettingsContent>
<FormFieldInputContainer>
<FormTextFieldInput
label={t`Field Name`}
placeholder={t`e.g., summary, status, count`}
defaultValue={field.name}
onChange={(value) =>
updateField(field.id, { name: value })
}
readonly={readonly}
/>
</FormFieldInputContainer>
<FormFieldInputContainer>
<WorkflowOutputFieldTypeSelector
onChange={(value) =>
updateField(field.id, { type: value })
}
value={field.type}
disabled={readonly}
dropdownId={`output-field-type-selector-${field.id}`}
/>
</FormFieldInputContainer>
<FormFieldInputContainer>
<FormTextFieldInput
label={t`Description`}
placeholder={t`Brief explanation of this output field`}
defaultValue={field.description}
onChange={(value) =>
updateField(field.id, { description: value })
}
readonly={readonly}
/>
</FormFieldInputContainer>
</StyledSettingsContent>
</StyledOutputSchemaFieldContainer>
);
})}
</StyledFieldsContainer>
)}
{!readonly && (
<StyledAddFieldButton onClick={addField}>
<IconPlus size={theme.icon.size.sm} />
{t`Add Output Field`}
</StyledAddFieldButton>
)}
</StyledOutputSchemaContainer>
);
};
@@ -0,0 +1,39 @@
import { InputSchemaPropertyType } from '@/workflow/types/InputSchema';
import { msg } from '@lingui/core/macro';
import { FieldMetadataType } from 'twenty-shared/types';
import {
IllustrationIconCalendarEvent,
IllustrationIconNumbers,
IllustrationIconText,
IllustrationIconToggle,
} from 'twenty-ui/display';
export interface OutputSchemaField {
id: string;
name: string;
description?: string;
type: InputSchemaPropertyType | undefined;
}
export const OUTPUT_FIELD_TYPE_OPTIONS = [
{
label: msg`Text`,
value: FieldMetadataType.TEXT,
Icon: IllustrationIconText,
},
{
label: msg`Number`,
value: FieldMetadataType.NUMBER,
Icon: IllustrationIconNumbers,
},
{
label: msg`Boolean`,
value: FieldMetadataType.BOOLEAN,
Icon: IllustrationIconToggle,
},
{
label: msg`Date`,
value: FieldMetadataType.DATE,
Icon: IllustrationIconCalendarEvent,
},
];
@@ -0,0 +1,14 @@
import { gql } from '@apollo/client';
export const UPDATE_ONE_AGENT = gql`
mutation UpdateOneAgent($input: UpdateAgentInput!) {
updateOneAgent(input: $input) {
id
name
description
prompt
modelId
responseFormat
}
}
`;
@@ -0,0 +1,14 @@
import { gql } from '@apollo/client';
export const FIND_ONE_AGENT = gql`
query FindOneAgent($id: UUID!) {
findOneAgent(input: { id: $id }) {
id
name
description
prompt
modelId
responseFormat
}
}
`;
@@ -0,0 +1,82 @@
import { useMutation, useQuery } from '@apollo/client';
import { useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { useDebouncedCallback } from 'use-debounce';
import { UPDATE_ONE_AGENT } from '../graphql/mutations/updateOneAgent';
import { FIND_ONE_AGENT } from '../graphql/queries/findOneAgent';
type AgentFormValues = {
name: string;
prompt: string;
modelId: string;
};
export const useAgentUpdateFormState = ({
agentId,
readonly = false,
}: {
agentId: string;
readonly?: boolean;
}) => {
const [formValues, setFormValues] = useState<AgentFormValues>({
name: '',
prompt: '',
modelId: '',
});
const { loading } = useQuery(FIND_ONE_AGENT, {
variables: { id: agentId },
skip: !agentId,
onCompleted: (data) => {
if (isDefined(data?.findOneAgent)) {
const agent = data.findOneAgent;
setFormValues({
name: agent.name,
prompt: agent.prompt,
modelId: agent.modelId,
});
}
},
});
const [updateAgent] = useMutation(UPDATE_ONE_AGENT);
const updateAgentMutation = async (updates: Partial<AgentFormValues>) => {
if (!agentId) {
return;
}
await updateAgent({
variables: {
input: {
id: agentId,
...updates,
},
},
});
};
const handleSave = useDebouncedCallback(async (formData: AgentFormValues) => {
await updateAgentMutation({
name: formData.name,
prompt: formData.prompt,
modelId: formData.modelId,
});
}, 500);
const handleFieldChange = async (field: string, value: string) => {
if (readonly) {
return;
}
setFormValues((prev) => ({ ...prev, [field]: value }));
await handleSave({ ...formValues, [field]: value });
};
return {
formValues,
handleFieldChange,
loading,
};
};
@@ -0,0 +1,70 @@
import { WorkflowAiAgentAction } from '@/workflow/types/Workflow';
import { OutputSchemaField } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/constants/output-field-type-options';
import { BaseOutputSchema } from '@/workflow/workflow-variables/types/StepOutputSchema';
import { useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { useDebouncedCallback } from 'use-debounce';
import { v4 } from 'uuid';
import { getFieldIcon } from '../utils/getFieldIcon';
export const useAiAgentOutputSchema = (
outputSchema?: BaseOutputSchema,
onActionUpdate?: (action: WorkflowAiAgentAction) => void,
action?: WorkflowAiAgentAction,
readonly?: boolean,
) => {
const [outputFields, setOutputFields] = useState<OutputSchemaField[]>(
Object.entries(outputSchema || {}).map(([name, field]) => ({
id: v4(),
name,
type: field.type,
description: field.description,
})),
);
const debouncedSave = useDebouncedCallback(
async (fields: OutputSchemaField[]) => {
if (readonly === true) {
return;
}
const newOutputSchema = fields.reduce<BaseOutputSchema>(
(schema, field) => {
if (isDefined(field.name)) {
schema[field.name] = {
isLeaf: true,
type: field.type,
value: null,
icon: getFieldIcon(field.type),
label: field.name,
description: field.description,
};
}
return schema;
},
{},
);
if (isDefined(onActionUpdate) && isDefined(action)) {
onActionUpdate({
...action,
settings: {
...action.settings,
outputSchema: newOutputSchema,
},
});
}
},
500,
);
const handleOutputSchemaChange = async (fields: OutputSchemaField[]) => {
setOutputFields(fields);
await debouncedSave(fields);
};
return {
handleOutputSchemaChange,
outputFields,
};
};
@@ -0,0 +1,14 @@
import { aiModelsState } from '@/client-config/states/aiModelsState';
import { useRecoilValue } from 'recoil';
import { SelectOption } from 'twenty-ui/input';
export const useAiModelOptions = (): SelectOption<string>[] => {
const aiModels = useRecoilValue(aiModelsState);
return aiModels
.map((model) => ({
value: model.modelId,
label: `${model.label} (${model.provider})`,
}))
.sort((a, b) => a.label.localeCompare(b.label));
};
@@ -0,0 +1,17 @@
import { InputSchemaPropertyType } from '@/workflow/types/InputSchema';
import { FieldMetadataType } from 'twenty-shared/types';
export const getFieldIcon = (fieldType?: InputSchemaPropertyType): string => {
switch (fieldType) {
case FieldMetadataType.TEXT:
return 'IconAbc';
case FieldMetadataType.NUMBER:
return 'IconText';
case FieldMetadataType.BOOLEAN:
return 'IconCheckbox';
case FieldMetadataType.DATE:
return 'IconCalendarEvent';
default:
return 'IconQuestionMark';
}
};
@@ -28,4 +28,9 @@ export const OTHER_ACTIONS: Array<{
type: 'HTTP_REQUEST',
icon: 'IconWorld',
},
{
label: 'AI Agent',
type: 'AI_AGENT',
icon: 'IconBrain',
},
];
@@ -0,0 +1,11 @@
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey } from '~/generated/graphql';
import { OTHER_ACTIONS } from '../constants/OtherActions';
export const useFilteredOtherActions = () => {
const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
return OTHER_ACTIONS.filter((action) => {
return action.type !== 'AI_AGENT' || isAiEnabled;
});
};
@@ -15,6 +15,9 @@ export const getActionHeaderTypeOrThrow = (actionType: WorkflowActionType) => {
return msg`Action`;
case 'HTTP_REQUEST':
return msg`HTTP Request`;
case 'AI_AGENT':
return msg`AI Agent`;
default:
assertUnreachable(actionType, `Unsupported action type: ${actionType}`);
}
@@ -21,6 +21,8 @@ export const getActionIconColorOrThrow = ({
return theme.font.color.tertiary;
case 'SEND_EMAIL':
return theme.color.blue;
case 'AI_AGENT':
return theme.color.pink;
default:
assertUnreachable(actionType, `Unsupported action type: ${actionType}`);
}