diff --git a/.changeset/empty-yaks-follow.md b/.changeset/empty-yaks-follow.md
new file mode 100644
index 000000000..66711106d
--- /dev/null
+++ b/.changeset/empty-yaks-follow.md
@@ -0,0 +1,5 @@
+---
+"@workflow/web": patch
+---
+
+Invoke the endpoints health check in web o11y and render result in toolbar
diff --git a/packages/web/src/app/layout-client.tsx b/packages/web/src/app/layout-client.tsx
index 286923356..b4e4a75d7 100644
--- a/packages/web/src/app/layout-client.tsx
+++ b/packages/web/src/app/layout-client.tsx
@@ -6,6 +6,7 @@ import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { ThemeProvider, useTheme } from 'next-themes';
import { useEffect, useRef } from 'react';
import { ConnectionStatus } from '@/components/display-utils/connection-status';
+import { EndpointsHealthStatus } from '@/components/display-utils/endpoints-health-status';
import { SettingsDropdown } from '@/components/settings-dropdown';
import { Toaster } from '@/components/ui/sonner';
import { buildUrlWithConfig, useQueryParamConfig } from '@/lib/config';
@@ -144,7 +145,8 @@ function LayoutContent({ children }: LayoutClientProps) {
-
+
+
diff --git a/packages/web/src/components/display-utils/endpoints-health-status.tsx b/packages/web/src/components/display-utils/endpoints-health-status.tsx
new file mode 100644
index 000000000..bad42f398
--- /dev/null
+++ b/packages/web/src/components/display-utils/endpoints-health-status.tsx
@@ -0,0 +1,221 @@
+'use client';
+
+import { AlertCircleIcon, CheckCircle2Icon, LoaderIcon } from 'lucide-react';
+import { useEffect, useState } from 'react';
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from '@/components/ui/tooltip';
+import type { WorldConfig } from '@/lib/config-world';
+
+interface EndpointsHealthStatusProps {
+ config: WorldConfig;
+}
+
+interface HealthCheckResult {
+ flow: 'pending' | 'success' | 'error';
+ step: 'pending' | 'success' | 'error';
+ flowMessage?: string;
+ stepMessage?: string;
+ checkedAt?: string;
+}
+
+const HEALTH_CHECK_SESSION_KEY = 'workflow-endpoints-health-check';
+
+function getSessionHealthCheck(configKey: string): HealthCheckResult | null {
+ try {
+ const stored = sessionStorage.getItem(
+ `${HEALTH_CHECK_SESSION_KEY}-${configKey}`
+ );
+ if (stored) {
+ return JSON.parse(stored);
+ }
+ } catch {
+ // Ignore sessionStorage errors (e.g., in SSR or private browsing)
+ }
+ return null;
+}
+
+function setSessionHealthCheck(
+ configKey: string,
+ result: HealthCheckResult
+): void {
+ try {
+ sessionStorage.setItem(
+ `${HEALTH_CHECK_SESSION_KEY}-${configKey}`,
+ JSON.stringify(result)
+ );
+ } catch {
+ // Ignore sessionStorage errors
+ }
+}
+
+function getConfigKey(config: WorldConfig): string {
+ // Create a unique key based on relevant config values
+ return `${config.backend || 'local'}-${config.port || '3000'}`;
+}
+
+async function checkEndpointHealth(
+ baseUrl: string,
+ endpoint: 'flow' | 'step'
+): Promise<{ success: boolean; message: string }> {
+ try {
+ const url = new URL(
+ `/.well-known/workflow/v1/${endpoint}?__health`,
+ baseUrl
+ );
+ const response = await fetch(url.toString(), {
+ method: 'POST',
+ // Short timeout for health checks
+ signal: AbortSignal.timeout(5000),
+ });
+
+ if (response.ok) {
+ const text = await response.text();
+ return { success: true, message: text };
+ }
+ return {
+ success: false,
+ message: `HTTP ${response.status}: ${response.statusText}`,
+ };
+ } catch (error) {
+ const message =
+ error instanceof Error ? error.message : 'Connection failed';
+ return { success: false, message };
+ }
+}
+
+export function EndpointsHealthStatus({ config }: EndpointsHealthStatusProps) {
+ const [healthCheck, setHealthCheck] = useState
({
+ flow: 'pending',
+ step: 'pending',
+ });
+ const [isChecking, setIsChecking] = useState(false);
+
+ useEffect(() => {
+ const configKey = getConfigKey(config);
+ const cached = getSessionHealthCheck(configKey);
+
+ // If we have a cached result from this session, use it
+ if (cached) {
+ setHealthCheck(cached);
+ return;
+ }
+
+ // Otherwise, perform the health check
+ const performHealthCheck = async () => {
+ setIsChecking(true);
+
+ // Determine base URL based on config
+ const port = config.port || '3000';
+ const baseUrl = `http://localhost:${port}`;
+
+ const [flowResult, stepResult] = await Promise.all([
+ checkEndpointHealth(baseUrl, 'flow'),
+ checkEndpointHealth(baseUrl, 'step'),
+ ]);
+
+ const result: HealthCheckResult = {
+ flow: flowResult.success ? 'success' : 'error',
+ step: stepResult.success ? 'success' : 'error',
+ flowMessage: flowResult.message,
+ stepMessage: stepResult.message,
+ checkedAt: new Date().toISOString(),
+ };
+
+ setHealthCheck(result);
+ setSessionHealthCheck(configKey, result);
+ setIsChecking(false);
+ };
+
+ performHealthCheck();
+ }, [config]);
+
+ const allSuccess =
+ healthCheck.flow === 'success' && healthCheck.step === 'success';
+ const anyError = healthCheck.flow === 'error' || healthCheck.step === 'error';
+ const isPending =
+ healthCheck.flow === 'pending' || healthCheck.step === 'pending';
+
+ const getStatusIcon = () => {
+ if (isChecking || isPending) {
+ return (
+
+ );
+ }
+ if (allSuccess) {
+ return ;
+ }
+ if (anyError) {
+ return ;
+ }
+ return ;
+ };
+
+ const getStatusText = () => {
+ if (isChecking || isPending) {
+ return 'Checking endpoints...';
+ }
+ if (allSuccess) {
+ return 'Endpoints healthy';
+ }
+ if (anyError) {
+ return 'Endpoint issues';
+ }
+ return 'Unknown status';
+ };
+
+ const getEndpointStatus = (status: 'pending' | 'success' | 'error') => {
+ if (status === 'success') {
+ return ;
+ }
+ if (status === 'error') {
+ return ;
+ }
+ return ;
+ };
+
+ return (
+
+
+
+ {getStatusIcon()}
+ {getStatusText()}
+
+
+
+
+
Workflow Endpoint Health
+
+
+ {getEndpointStatus(healthCheck.flow)}
+ /.well-known/workflow/v1/flow
+
+ {healthCheck.flowMessage && (
+
+ {healthCheck.flowMessage}
+
+ )}
+
+
+
+ {getEndpointStatus(healthCheck.step)}
+ /.well-known/workflow/v1/step
+
+ {healthCheck.stepMessage && (
+
+ {healthCheck.stepMessage}
+
+ )}
+
+ {healthCheck.checkedAt && (
+
+ Checked at {new Date(healthCheck.checkedAt).toLocaleTimeString()}
+
+ )}
+
+
+
+ );
+}