Skip to content
Open
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
6 changes: 4 additions & 2 deletions apps/ai-studio/src/nodes/ai-agent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ export const aiAgentPaletteItem: PaletteItem<AiAgentSchema> = {
// Lets `{{ nodes.<id>.response }}` references resolve to a real mention instead of a "missing mention" pill.
outputSchema: {
type: 'default',
properties: {
response: { type: 'string', label: 'Response', description: 'The text generated by the AI model' },
bySourceHandle: {
success: {
response: { type: 'string', label: 'Response', description: 'The text generated by the AI model' },
},
},
},
};
17 changes: 15 additions & 2 deletions apps/demo/src/app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import type {
WorkflowBuilderNodeTemplates,
WorkflowBuilderReactFlowProps,
} from '@workflowbuilder/sdk';
import { SnackbarType } from '@workflowbuilder/ui';

import '@workflowbuilder/sdk/style.css';

import { showSnackbar } from '../../../../packages/sdk/src/utils/show-snackbar';
import { DashedEdge } from './components/dashed-edge/dashed-edge';
import { MultiPortNodeTemplate } from './components/multi-port-node/multi-port-node-template';
import { demoPaletteItems } from './data/palette';
Expand Down Expand Up @@ -35,8 +37,19 @@ const edgeTemplates = {
dashed: DashedEdge,
} satisfies WorkflowBuilderEdgeTemplates;

// A trigger is a workflow entry point, so it can never be a connection target.
const isValidConnection: WorkflowBuilderIsValidConnection = ({ targetNode }) => targetNode.data.type !== 'trigger';
const isValidConnection: WorkflowBuilderIsValidConnection = ({ targetNode }) => {
// A trigger is a workflow entry point, so it can never be a connection target.
if (targetNode.data.type === 'trigger') {
showSnackbar({
title: 'notValidConnection',
variant: SnackbarType.WARNING,
});

return false;
}

return true;
};

// Advanced escape hatch: forward extra ReactFlow props (SDK-owned props can't be set here).
const reactFlowProps = {
Expand Down
13 changes: 9 additions & 4 deletions apps/demo/src/app/data/nodes/action/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,15 @@ export const action: PaletteItem<ActionNodeSchema> = {
uischema,
outputSchema: {
type: 'default',
properties: {
status: { type: 'string', label: 'Status', description: 'Execution status: success, failure, or skipped' },
result: { type: 'object', label: 'Result', description: 'The data returned by the action' },
errorMessage: { type: 'string', label: 'Error Message', description: 'Error details if the action failed' },
bySourceHandle: {
success: {
status: { type: 'string', label: 'Status', description: 'Execution status: success, failure, or skipped' },
// TODO: outputSchema and schema properties should support the full JsonSchema7 type imported from @jsonforms/core to build suggestions not only for objects but also for their variables.

@szymon-t-sc szymon-t-sc Aug 14, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While the current format of suggestions is simple, its simplicity is limiting. For example, if we used a schema format similar to the node schema, with nesting and options, we could support much richer suggestions.

Even for simple non-object properties, such as strings, the schema can define options. We could support those here as well, allowing the condition builder to show a select with predefined options instead of an empty text field when someone picks variable received from the node in condition node later.

For object properties, returning just 'object' doesn't give us much to work with. If the object includes defined properties, we can build conditions based on those nested properties.

We should replace this format with a schema and use it similarly to how we use node schemas: to build dedicated controls for conditions and for 'set a value' nodes, allowing properties to be overridden while following the schema's types.

result: { type: 'object', label: 'Result', description: 'The data returned by the action' },
},
error: {
errorMessage: { type: 'string', label: 'Error Message', description: 'Error details if the action failed' },
},
},
},
};
8 changes: 4 additions & 4 deletions apps/demo/src/app/data/nodes/action/uischema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,16 @@ const sendEmailProperties: ActionNodeUISchema = {
placeholder: 'manager@example.com',
},
{
type: 'Text',
type: 'VariableText',
scope: scope('properties.sendEmail.properties.subject'),
label: 'Subject',
placeholder: 'Type your subject here...',
placeholder: 'Type your subject here... Use {{ to insert variables',
},
{
type: 'TextArea',
type: 'VariableTextArea',
scope: scope('properties.sendEmail.properties.body'),
label: 'Email Body',
placeholder: 'Type your message here...',
placeholder: 'Type your message here... Use {{ to insert variables',
minRows: 5,
},
{
Expand Down
10 changes: 6 additions & 4 deletions apps/demo/src/app/data/nodes/ai-agent/ai-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ export const aiAgent: PaletteItem = {
uischema,
outputSchema: {
type: 'default',
properties: {
response: { type: 'string', label: 'Response', description: 'The text generated by the AI model' },
tokensUsed: { type: 'number', label: 'Tokens Used', description: 'Total number of tokens consumed' },
model: { type: 'string', label: 'Model', description: 'The AI model that was used' },
bySourceHandle: {
success: {
response: { type: 'string', label: 'Response', description: 'The text generated by the AI model' },
tokensUsed: { type: 'number', label: 'Tokens Used', description: 'Total number of tokens consumed' },
model: { type: 'string', label: 'Model', description: 'The AI model that was used' },
},
},
},
};
14 changes: 8 additions & 6 deletions apps/demo/src/app/data/nodes/conditional/conditional.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ export const conditional: PaletteItem<ConditionalNodeSchema> = {
uischema,
outputSchema: {
type: 'default',
properties: {
result: { type: 'boolean', label: 'Result', description: 'Whether the condition evaluated to true or false' },
matchedCondition: {
type: 'string',
label: 'Matched Condition',
description: 'The condition expression that matched',
bySourceHandle: {
success: {
result: { type: 'boolean', label: 'Result', description: 'Whether the condition evaluated to true or false' },
matchedCondition: {
type: 'string',
label: 'Matched Condition',
description: 'The condition expression that matched',
},
},
},
},
Expand Down
8 changes: 5 additions & 3 deletions apps/demo/src/app/data/nodes/decision/decision.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ export const decision: PaletteItem<DecisionNodeSchema> = {
uischema,
outputSchema: {
type: 'default',
properties: {
selectedBranch: { type: 'string', label: 'Selected Branch', description: 'Label of the branch that was taken' },
branchIndex: { type: 'number', label: 'Branch Index', description: 'Zero-based index of the selected branch' },
bySourceHandle: {
every: {
selectedBranch: { type: 'string', label: 'Selected Branch', description: 'Label of the branch that was taken' },
branchIndex: { type: 'number', label: 'Branch Index', description: 'Zero-based index of the selected branch' },
},
},
},
};
8 changes: 5 additions & 3 deletions apps/demo/src/app/data/nodes/delay/delay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ export const delay: PaletteItem<DelayNodeSchema> = {
uischema,
outputSchema: {
type: 'default',
properties: {
resumedAt: { type: 'string', label: 'Resumed At', description: 'ISO 8601 date-time when the delay ended' },
delayDuration: { type: 'number', label: 'Delay Duration', description: 'Actual wait time in milliseconds' },
bySourceHandle: {
success: {
resumedAt: { type: 'string', label: 'Resumed At', description: 'ISO 8601 date-time when the delay ended' },
delayDuration: { type: 'number', label: 'Delay Duration', description: 'Actual wait time in milliseconds' },
},
},
},
};
14 changes: 10 additions & 4 deletions apps/demo/src/app/data/nodes/notification/notification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,16 @@ export const notification: PaletteItem<NotificationNodeSchema> = {
uischema,
outputSchema: {
type: 'default',
properties: {
sent: { type: 'boolean', label: 'Sent', description: 'Whether the notification was sent successfully' },
sentAt: { type: 'string', label: 'Sent At', description: 'ISO 8601 date-time when the notification was sent' },
recipient: { type: 'string', label: 'Recipient', description: 'The email address the notification was sent to' },
bySourceHandle: {
success: {
sent: { type: 'boolean', label: 'Sent', description: 'Whether the notification was sent successfully' },
sentAt: { type: 'string', label: 'Sent At', description: 'ISO 8601 date-time when the notification was sent' },
recipient: {
type: 'string',
label: 'Recipient',
description: 'The email address the notification was sent to',
},
},
},
},
};
64 changes: 58 additions & 6 deletions apps/demo/src/app/data/nodes/trigger/trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,63 @@ export const triggerNode: PaletteItem<TriggerNodeSchema> = {
schema,
uischema,
outputSchema: {
type: 'default',
properties: {
eventType: { type: 'string', label: 'Event Type', description: 'The type of event that started the workflow' },
timestamp: { type: 'string', label: 'Timestamp', description: 'ISO 8601 date-time when the trigger fired' },
payload: { type: 'object', label: 'Payload', description: 'The raw event data received by the trigger' },
},
type: 'variant',
variants: [
{
variantRule: undefined,
bySourceHandle: {
every: {
eventType: {
type: 'string',
label: 'Event Type',
description: 'The type of event that started the workflow',
},
timestamp: { type: 'string', label: 'Timestamp', description: 'ISO 8601 date-time when the trigger fired' },
},
},
},
{
variantRule: {
dataPropertyName: 'type',
dataPropertyValue: 'timeBasedTrigger',
},
bySourceHandle: {
success: {
allDay: {
type: 'boolean',
label: 'All day event',
description: 'The type of event that started the workflow',
},
startDate: {
type: 'date',
label: 'Start date',
description: 'The date when the event was scheduled to start',
},
endDate: {
type: 'date',
label: 'End date',
description: 'The date when the event was scheduled to end',
},
},
},
},
{
variantRule: {
dataPropertyName: 'type',
dataPropertyValue: 'eventBasedTrigger',
},
bySourceHandle: {
success: {
typeOfEventType: {
type: 'string',
label: 'Type of event type',
description: 'For example: form submission, user action etc.',
},
},
},
},
],
},
};

// payload: { type: 'object', label: 'Payload', description: 'The raw event data received by the trigger' },
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import { Icon } from '@workflow-builder/icons';

import styles from '../../app-bar.module.css';

import { openModalWorkflowSettings } from '../../../../features/variables/modals/modal-settings';
import { useStore } from '../../../../store/store';
import { withOptionalComponentPlugins } from '../../../plugins-core/adapters/adapter-components';
import { openModalWorkflowSettings } from '../../../variables/modals/global/modal-settings';

/**
* Props accepted by {@link ProjectSelection}. Use this when typing a
Expand Down
3 changes: 3 additions & 0 deletions packages/sdk/src/features/diagram/diagram.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import type { WorkflowBuilderReactFlowProps } from '../../workflow-builder-root/
import { trackFutureChange } from '../changes-tracker/stores/use-changes-tracker-store';
import { useDeleteConfirmation } from '../modals/delete-confirmation/use-delete-confirmation';
import { withOptionalComponentPlugins } from '../plugins-core/adapters/adapter-components';
import useRefreshVariables from '../variables/hooks/use-refresh-variables';
import { deleteKeyCode } from './const';
import { SNAP_GRID, SNAP_IS_ACTIVE } from './diagram.const';
import { TemporaryEdge } from './edges/temporary-edge/temporary-edge';
Expand Down Expand Up @@ -126,6 +127,8 @@ function DiagramContainerComponent({ edgeTypes = {} }: DiagramContainerProps) {
[onDropFromPalette],
);

useRefreshVariables();

const { onConnect, onConnectStart, onConnectEnd } = useConnect();

const onNodeDragStop = useCallback(() => {
Expand Down
6 changes: 6 additions & 0 deletions packages/sdk/src/features/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,9 @@ export const en = {
variableNotFound: 'Variable not found.',
removeVariableWarning: 'Deleting this variable will permanently remove its configuration.',
removeVariableIsBlocked: 'The variable is used in the following nodes and cannot be deleted.',
addVariableToContinue: 'Add a variable to continue',
missingMentionNodePrefix: 'Missing node',
missingMentionNodeVariablePrefix: 'Missing variable',
},
loader: {
text: 'Loading...',
Expand Down Expand Up @@ -181,7 +184,10 @@ export const en = {
wrongDiagramFormat: 'Wrong diagram format',
contentCopied: 'Content copied to clipboard',
variablesListIsEmpty: 'The list of available variables is empty.',
variableNameAlreadyExists: 'A variable with this name already exists.',
variableWasNotFound: 'This variable was not found.',
cantEditReadOnlyMode: 'Editing is blocked in read-only mode.',
notValidConnection: 'That connection is blocked.',
},
workflowsSettings: {
modalTitle: 'Settings',
Expand Down
6 changes: 6 additions & 0 deletions packages/sdk/src/features/i18n/locales/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ export const pl = {
variableNotFound: 'Nie znaleziono zmiennej.',
removeVariableWarning: 'Usunięcie tej zmiennej trwale usunie jej konfigurację.',
removeVariableIsBlocked: 'Ta zmienna jest używana w następujących węzłach i nie może zostać usunięta.',
addVariableToContinue: 'Dodaj zmienną aby kontynuować',
missingMentionNodePrefix: 'Brak węzła',
missingMentionNodeVariablePrefix: 'Brak zmiennej',
},
loader: {
text: 'Ładowanie...',
Expand Down Expand Up @@ -145,7 +148,10 @@ export const pl = {
wrongDiagramFormat: 'Nieprawidłowy format diagramu',
contentCopied: 'Treść skopiowana do schowka',
variablesListIsEmpty: 'Lista dostępnych zmiennych jest pusta.',
variableNameAlreadyExists: 'Zmienna o tej nazwie już istnieje.',
variableWasNotFound: 'Nie znaleziono tej zmiennej.',
cantEditReadOnlyMode: 'Edycja jest zablokowana w trybie tylko do odczytu.',
notValidConnection: 'To połączenie jest zablokowane.',
},
aiTools: {
title: 'Narzędzia agenta AI',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import styles from './dependencies.module.css';

import { FormControlWithLabel } from '../../../../../components/form/form-control-with-label/form-control-with-label';
import { useSingleSelectedElement } from '../../../../../features/properties-bar/use-single-selected-element';
import { conditionsToDependencies } from '../../../../../features/variables/actions/conditions';
import { VariableText } from '../../../../../features/variables/components/variable-text/variable-text';
import { useAvailableVariables } from '../../../../../features/variables/hooks/use-available-variables';
import type { DynamicCondition } from '../../../../../types/controls';
import { noop } from '../../../../../utils/noop';
import { conditionsToDependencies } from '../../../utils/conditional-transform';

type Props = {
conditions: DynamicCondition[];
Expand All @@ -23,12 +23,13 @@ export function Dependencies({ conditions, onClick, disabled = false, hasError }
}, [conditions]);

const selection = useSingleSelectedElement();
const suggestionGroups = useAvailableVariables(selection?.node?.id);
const { suggestionGroups, totalVariables } = useAvailableVariables(selection?.node?.id);

return (
<FormControlWithLabel label="conditions.dependencies">
<span className={styles['button']} onClick={disabled ? noop : onClick}>
<VariableText
key={totalVariables}
className={styles['list']}
value={dependencies.join(' ')}
onChange={noop}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,18 @@ import { Icon } from '@workflow-builder/icons';

import styles from './conditions-form-field.module.css';

import { type ConditionErrors, getConditionErrors } from '../../../../../features/variables/actions/conditions';
import { getStringType } from '../../../../../features/variables/actions/get-string-type';
import { DynamicTypedVariableOrInput } from '../../../../../features/variables/components/dynamic-typed-variable-or-input/dynamic-typed-variable-or-input';
import { VariableText } from '../../../../../features/variables/components/variable-text/variable-text';
import type { VariableSuggestionGroup } from '../../../../../features/variables/components/variable-text/variable-text.types';
import {
type ComparisonOperator,
LOGICAL_OPERATOR,
comparisonOperatorsByPrimitiveType,
} from '../../../../../features/variables/constants';
import type { VariableTypePrimitive } from '../../../../../node/node-output-schema';
import type { DynamicCondition } from '../../../../../types/controls';
import { getStringVariableTypeIfPossible } from '../../../../variables/actions/get-string-variable-type-if-possible';
import { type ConditionErrors, getConditionErrors } from '../../../../variables/utils/form-validation/conditions';

type ConditionsFormFieldProps = {
condition: Partial<DynamicCondition>;
Expand All @@ -34,7 +35,7 @@ const getTypeOptions = (
xType: VariableTypePrimitive;
comparisonsOperators: ComparisonOperator[];
} => {
const xType = getStringType(value);
const xType = getStringVariableTypeIfPossible(value);
const comparisonsOperators: ComparisonOperator[] = comparisonOperatorsByPrimitiveType[xType] || [];

return {
Expand Down Expand Up @@ -78,11 +79,11 @@ export function ConditionsFormField(props: ConditionsFormFieldProps) {
<SegmentPicker
className={styles['segment-picker']}
size="xx-small"
value={condition.logicalOperator || 'AND'}
value={condition.logicalOperator || LOGICAL_OPERATOR.AND}
onChange={(_, value) => handleChange('logicalOperator', value)}
>
<SegmentPicker.Item value="AND">{t('conditions.compare.all')}</SegmentPicker.Item>
<SegmentPicker.Item value="OR">{t('conditions.compare.one')}</SegmentPicker.Item>
<SegmentPicker.Item value={LOGICAL_OPERATOR.AND}>{t('conditions.compare.all')}</SegmentPicker.Item>
<SegmentPicker.Item value={LOGICAL_OPERATOR.OR}>{t('conditions.compare.one')}</SegmentPicker.Item>
</SegmentPicker>
</div>
)}
Expand Down
Loading
Loading