Portal Community

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 };
  }
}