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
16 changes: 16 additions & 0 deletions .changeset/flow-action-messages.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
"@objectstack/spec": minor
"@objectstack/service-automation": minor
---

feat(automation): declarative screen-flow completion/error messages + action `errorMessage`

A screen flow can now declare `successMessage` / `errorMessage` (FlowSchema). The
engine surfaces them on the terminal `AutomationResult` (`successMessage` on
success, `errorMessage` on failure), so the UI flow-runner shows a meaningful
toast instead of a generic "Done" / the raw error — no manual "success screen"
node needed. The CRM convert-lead wizard sets a friendly completion message.

Also exposes `errorMessage` on the UI Action schema. The runtime (ActionRunner)
already honoured it; it just wasn't declarable in the spec — closing a
spec↔runtime gap so authors can set a friendly failure toast.
5 changes: 5 additions & 0 deletions examples/app-crm/src/flows/convert-lead.flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ export const ConvertLeadScreenFlow = defineFlow({
status: 'active',
runAs: 'user',

// Friendly terminal toasts — the flow-runner shows these instead of a generic
// "Done" / the raw error when the wizard finishes.
successMessage: '🎉 Lead converted — customer and opportunity created.',
errorMessage: 'Lead conversion did not finish — review the lead and try again.',

variables: [
// ── input (from the action trigger) ───────────────────────────────────
{ name: 'recordId', type: 'text', isInput: true, isOutput: false },
Expand Down
8 changes: 6 additions & 2 deletions packages/services/service-automation/src/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1287,7 +1287,9 @@ export class AutomationEngine implements IAutomationService {
await this.bubbleToParent(run, output);
}

return { success: true, output, durationMs };
// Surface the flow's friendly completion message so a screen-flow
// runner shows it instead of a generic "Done".
return { success: true, output, durationMs, successMessage: flow.successMessage };
} catch (err: unknown) {
// Re-suspended at a downstream node: persist a fresh continuation.
if (isSuspendSignal(err)) {
Expand Down Expand Up @@ -1341,7 +1343,9 @@ export class AutomationEngine implements IAutomationService {
if (!skipBubble) {
await this.failAncestors(run.context, errorMessage);
}
return { success: false, error: errorMessage, durationMs };
// Surface the flow's friendly error message (the raw error stays
// in `error` for logs/diagnostics).
return { success: false, error: errorMessage, durationMs, errorMessage: flow.errorMessage };
}
} finally {
this.resuming.delete(runId);
Expand Down
12 changes: 11 additions & 1 deletion packages/spec/src/automation/flow.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,17 @@ export const FlowSchema = lazySchema(() => z.object({
name: z.string().regex(/^[a-z_][a-z0-9_]*$/).describe('Machine name'),
label: z.string().describe('Flow label'),
description: z.string().optional(),


/**
* Terminal messages for `screen`-flow runs. When the run reaches a terminal
* state, the UI flow-runner shows `successMessage` instead of a generic
* "Done" toast, and `errorMessage` instead of the raw error. Both are
* surfaced on the terminal {@link AutomationResult} (`successMessage` /
* `errorMessage`). Plain strings; `{var}` is NOT interpolated here.
*/
successMessage: z.string().optional().describe('Toast shown when a screen flow completes (defaults to a generic "Done").'),
errorMessage: z.string().optional().describe('Toast shown when a screen flow fails (defaults to the raw error).'),

/** Metadata & Versioning */
version: z.number().int().default(1).describe('Version number'),
status: z.enum(['draft', 'active', 'obsolete', 'invalid']).default('draft').describe('Deployment status'),
Expand Down
8 changes: 8 additions & 0 deletions packages/spec/src/contracts/automation-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,14 @@ export interface AutomationResult {
* `signal.variables`.
*/
screen?: ScreenSpec;
/**
* Friendly terminal messages copied from the flow definition
* (`flow.successMessage` / `flow.errorMessage`) so a screen-flow runner can
* show a meaningful toast instead of a generic "Done" / the raw error.
* `successMessage` is set on terminal success, `errorMessage` on failure.
*/
successMessage?: string;
errorMessage?: string;
}

/** Signal payload used to resume a paused run (ADR-0019). */
Expand Down
3 changes: 3 additions & 0 deletions packages/spec/src/ui/action.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,9 @@ export const ActionSchema = lazySchema(() => z.object({
/** UX Behavior */
confirmText: I18nLabelSchema.optional().describe('Confirmation message before execution'),
successMessage: I18nLabelSchema.optional().describe('Success message to show after execution'),
// Runtime (ActionRunner) already honours this — declared here so authors can
// set a friendly failure toast instead of surfacing the raw error string.
errorMessage: I18nLabelSchema.optional().describe('Error message to show when the action fails (overrides the raw error).'),
refreshAfter: z.boolean().default(false).describe('Refresh view after execution'),

/**
Expand Down
Loading