EdgeStream
UI Action Plugin
The UI Action Plugin owns the ui.* topic namespace. It carries UI interaction events from the server to client or between clients — server-driven UI updates, form rendering instructions, and interactive dialog triggers.
Topic Namespace
ui.render.form // Server instructs client to render a form
ui.render.dialog // Server instructs client to show a dialog
ui.render.notification // Server instructs client to show a notification
ui.action.navigate // Server instructs client to navigate to a URL/route
ui.action.refresh // Server instructs client to refresh a component/page
ui.interaction.formSubmit // Client reports a form was submitted
ui.interaction.buttonClick // Client reports a button was clicked
Server-Driven Form Rendering
The ui.render.form topic is the key message type for HIL (Human in the Loop) workflows. When a workflow is suspended awaiting user input, the server sends a ui.render.form message that causes the client to display the appropriate Atlas Form:
// Message format: ui.render.form
interface UiRenderFormMessage {
formId: string; // Atlas Form ID to render
formVersion?: number;
contextData?: Record<string, unknown>; // Pre-filled data
submissionTopic: string; // Topic to publish the response to
expiresAt?: string; // Form expires after this time
targetUserId?: string; // Who should see this form
}
// Subscribe to form rendering instructions
stream.subscribe('bas', 'ui.render.form', (envelope) => {
const { formId, contextData, submissionTopic } = envelope.body;
// Render the form in a modal or panel
uiStore.openForm({
formId,
data: contextData,
onSubmit: async (formData) => {
await stream.send('bas', submissionTopic, formData);
},
});
});
Server-Driven Navigation
// ui.action.navigate — server redirects the client
interface UiNavigateMessage {
url?: string; // absolute URL
route?: string; // app router route (e.g., '/workflow/exec-123')
params?: Record<string, string>;
replace?: boolean; // replace history entry (no back button)
}
stream.subscribe('bas', 'ui.action.navigate', (envelope) => {
const { route, params, replace } = envelope.body;
if (replace) {
router.replace(route, { params });
} else {
router.push(route, { params });
}
});
Server-Driven Notifications
// ui.render.notification
interface UiNotificationMessage {
type: 'info' | 'success' | 'warning' | 'error';
title: string;
message: string;
duration?: number; // ms before auto-dismiss (0 = persistent)
action?: {
label: string;
topic: string; // publish to this topic when action clicked
};
}
stream.subscribe('bas', 'ui.render.notification', (envelope) => {
const { type, title, message, duration } = envelope.body;
toastService.show({ type, title, message, duration });
});
IUiQueryHook Integration
The IUiQueryHook interface in the pipeline is the hook-level counterpart to the UI Action Plugin. It works with IPipelineContext.pause() to suspend the pipeline while waiting for user input:
// UiQueryHook pauses the pipeline — sends ui.render.form, awaits response
class UiQueryHook implements IUiQueryHook {
readonly name = 'UiQueryHook';
readonly priority = 150; // processing stage
async execute(context: IPipelineContext): Promise<HookResult> {
if (!this.requiresUserInput(context.body)) {
return { continue: true };
}
// Pause the pipeline — delivers the form to the user
const pause = context.pause({
id: generateId(),
type: 'form',
message: 'Approval required',
payload: { formId: 'approval-form-001', data: context.body },
timeoutMs: 300000, // 5 minute timeout
});
// Pipeline is frozen here until resume() or reject() is called
const response = await pause.resume;
// Continue with user's response in context
context.body = { ...context.body, userResponse: response };
return { continue: true };
}
}