diff --git a/code/addons/interactions/src/components/InteractionsPanel.stories.tsx b/code/addons/interactions/src/components/InteractionsPanel.stories.tsx
index b9273b5a04f..d228b177a81 100644
--- a/code/addons/interactions/src/components/InteractionsPanel.stories.tsx
+++ b/code/addons/interactions/src/components/InteractionsPanel.stories.tsx
@@ -56,6 +56,17 @@ export default meta;
 type Story = StoryObj<typeof meta>;
 
 export const Passing: Story = {
+  // TODO: Remove after prototyping
+  beforeEach: async ({ reporting }) => {
+    reporting.addReport({
+      id: 'a11y',
+      status: 'passed',
+      version: 1,
+      result: {
+        violations: [{ id: 'a11y', impact: 'critical', description: 'A11y violation' }],
+      },
+    });
+  },
   args: {
     interactions: getInteractions(CallStates.DONE),
   },
diff --git a/code/addons/test/src/node/reporter.ts b/code/addons/test/src/node/reporter.ts
index 833ab0bf382..e83d2ee3f54 100644
--- a/code/addons/test/src/node/reporter.ts
+++ b/code/addons/test/src/node/reporter.ts
@@ -6,6 +6,7 @@ import type {
   TestingModuleProgressReportPayload,
   TestingModuleProgressReportProgress,
 } from 'storybook/internal/core-events';
+import type { Report } from 'storybook/internal/preview-api';
 
 import type { API_StatusUpdate } from '@storybook/types';
 
@@ -26,6 +27,7 @@ export type TestResultResult =
       storyId: string;
       testRunId: string;
       duration: number;
+      reports: Report[];
     }
   | {
       status: 'failed';
@@ -33,6 +35,7 @@ export type TestResultResult =
       duration: number;
       testRunId: string;
       failureMessages: string[];
+      reports: Report[];
     };
 
 export type TestResult = {
@@ -113,16 +116,26 @@ export class StorybookReporter implements Reporter {
 
         const status = StatusMap[t.result?.state || t.mode] || 'skipped';
         const storyId = (t.meta as any).storyId as string;
+        const reports = (t.meta as any).reports as Report[];
         const duration = t.result?.duration || 0;
         const testRunId = this.start.toString();
 
         switch (status) {
           case 'passed':
           case 'pending':
-            return [{ status, storyId, duration, testRunId } as TestResultResult];
+            return [{ status, storyId, duration, testRunId, reports } as TestResultResult];
           case 'failed':
             const failureMessages = t.result?.errors?.map((e) => e.stack || e.message) || [];
-            return [{ status, storyId, duration, failureMessages, testRunId } as TestResultResult];
+            return [
+              {
+                status,
+                storyId,
+                duration,
+                failureMessages,
+                testRunId,
+                reports,
+              } as TestResultResult,
+            ];
           default:
             return [];
         }
diff --git a/code/addons/test/src/vitest-plugin/test-utils.ts b/code/addons/test/src/vitest-plugin/test-utils.ts
index cdd199d3998..9ac6fbb487e 100644
--- a/code/addons/test/src/vitest-plugin/test-utils.ts
+++ b/code/addons/test/src/vitest-plugin/test-utils.ts
@@ -3,7 +3,7 @@
 /* eslint-disable no-underscore-dangle */
 import { type RunnerTask, type TaskContext, type TaskMeta, type TestContext } from 'vitest';
 
-import { composeStory } from 'storybook/internal/preview-api';
+import { type Report, composeStory } from 'storybook/internal/preview-api';
 import type { ComponentAnnotations, ComposedStoryFn } from 'storybook/internal/types';
 
 import { setViewport } from './viewports';
@@ -22,10 +22,14 @@ export const testStory = (
 
     context.story = composedStory;
 
-    const _task = context.task as RunnerTask & { meta: TaskMeta & { storyId: string } };
+    const _task = context.task as RunnerTask & {
+      meta: TaskMeta & { storyId: string; reports: Report[] };
+    };
     _task.meta.storyId = composedStory.id;
 
     await setViewport(composedStory.parameters, composedStory.globals);
     await composedStory.run();
+
+    _task.meta.reports = composedStory.reporting.reports;
   };
 };
diff --git a/code/core/src/core-events/index.ts b/code/core/src/core-events/index.ts
index d5d51eeecdf..e22b5207c18 100644
--- a/code/core/src/core-events/index.ts
+++ b/code/core/src/core-events/index.ts
@@ -32,6 +32,7 @@ enum events {
   STORY_CHANGED = 'storyChanged',
   STORY_UNCHANGED = 'storyUnchanged',
   STORY_RENDERED = 'storyRendered',
+  STORY_COMPLETED = 'storyCompleted',
   STORY_MISSING = 'storyMissing',
   STORY_ERRORED = 'storyErrored',
   STORY_THREW_EXCEPTION = 'storyThrewException',
@@ -140,6 +141,7 @@ export const {
   STORY_PREPARED,
   STORY_RENDER_PHASE_CHANGED,
   STORY_RENDERED,
+  STORY_COMPLETED,
   STORY_SPECIFIED,
   STORY_THREW_EXCEPTION,
   STORY_UNCHANGED,
diff --git a/code/core/src/manager/globals/exports.ts b/code/core/src/manager/globals/exports.ts
index b9141e853f9..5516b01c574 100644
--- a/code/core/src/manager/globals/exports.ts
+++ b/code/core/src/manager/globals/exports.ts
@@ -797,6 +797,7 @@ export default {
     'STORIES_EXPAND_ALL',
     'STORY_ARGS_UPDATED',
     'STORY_CHANGED',
+    'STORY_COMPLETED',
     'STORY_ERRORED',
     'STORY_INDEX_INVALIDATED',
     'STORY_MISSING',
@@ -861,6 +862,7 @@ export default {
     'STORIES_EXPAND_ALL',
     'STORY_ARGS_UPDATED',
     'STORY_CHANGED',
+    'STORY_COMPLETED',
     'STORY_ERRORED',
     'STORY_INDEX_INVALIDATED',
     'STORY_MISSING',
@@ -925,6 +927,7 @@ export default {
     'STORIES_EXPAND_ALL',
     'STORY_ARGS_UPDATED',
     'STORY_CHANGED',
+    'STORY_COMPLETED',
     'STORY_ERRORED',
     'STORY_INDEX_INVALIDATED',
     'STORY_MISSING',
diff --git a/code/core/src/preview-api/index.ts b/code/core/src/preview-api/index.ts
index 0a61c7333ab..d067acad89c 100644
--- a/code/core/src/preview-api/index.ts
+++ b/code/core/src/preview-api/index.ts
@@ -60,6 +60,6 @@ export { createPlaywrightTest } from './modules/store/csf/portable-stories';
 export type { PropDescriptor } from './store';
 
 /** STORIES API */
-export { StoryStore } from './store';
+export { StoryStore, type Report, ReporterAPI } from './store';
 export { Preview, PreviewWeb, PreviewWithSelection, UrlStore, WebView } from './preview-web';
 export type { SelectionStore, View } from './preview-web';
diff --git a/code/core/src/preview-api/modules/preview-web/render/StoryRender.ts b/code/core/src/preview-api/modules/preview-web/render/StoryRender.ts
index 3441a5c64e9..895674b6b73 100644
--- a/code/core/src/preview-api/modules/preview-web/render/StoryRender.ts
+++ b/code/core/src/preview-api/modules/preview-web/render/StoryRender.ts
@@ -14,6 +14,7 @@ import type {
 
 import {
   PLAY_FUNCTION_THREW_EXCEPTION,
+  STORY_COMPLETED,
   STORY_RENDERED,
   STORY_RENDER_PHASE_CHANGED,
   UNHANDLED_ERRORS_WHILE_PLAYING,
@@ -34,6 +35,7 @@ export type RenderPhase =
   | 'playing'
   | 'played'
   | 'completed'
+  | 'finished'
   | 'aborted'
   | 'errored';
 
@@ -288,7 +290,7 @@ export class StoryRender<TRenderer extends Renderer> implements Render<TRenderer
       const ignoreUnhandledErrors =
         this.story.parameters?.test?.dangerouslyIgnoreUnhandledErrors === true;
 
-      const unhandledErrors: Set<unknown> = new Set();
+      const unhandledErrors: Set<unknown> = new Set<unknown>();
       const onError = (event: ErrorEvent | PromiseRejectionEvent) =>
         unhandledErrors.add('error' in event ? event.error : event.reason);
 
@@ -349,9 +351,30 @@ export class StoryRender<TRenderer extends Renderer> implements Render<TRenderer
       await this.runPhase(abortSignal, 'completed', async () =>
         this.channel.emit(STORY_RENDERED, id)
       );
+
+      // The event name 'completed' is unfortunately already reserved by the STORY_RENDERED event
+      await this.runPhase(abortSignal, 'finished', async () =>
+        this.channel.emit(STORY_COMPLETED, {
+          storyId: id,
+          unhandledExceptions: !ignoreUnhandledErrors
+            ? Array.from(unhandledErrors).map(serializeError)
+            : [],
+          status: !ignoreUnhandledErrors && unhandledErrors.size > 0 ? 'error' : 'success',
+          reporters: context.reporting.reports,
+        })
+      );
     } catch (err) {
       this.phase = 'errored';
       this.callbacks.showException(err as Error);
+
+      await this.runPhase(abortSignal, 'finished', async () =>
+        this.channel.emit(STORY_COMPLETED, {
+          storyId: id,
+          unhandledExceptions: [serializeError(err)],
+          status: 'error',
+          reporters: [],
+        })
+      );
     }
 
     // If a rerender was enqueued during the render, clear the queue and render again
diff --git a/code/core/src/preview-api/modules/store/StoryStore.ts b/code/core/src/preview-api/modules/store/StoryStore.ts
index cd7dc40a6ed..01404a5f1bf 100644
--- a/code/core/src/preview-api/modules/store/StoryStore.ts
+++ b/code/core/src/preview-api/modules/store/StoryStore.ts
@@ -45,6 +45,7 @@ import {
   prepareStory,
   processCSFFile,
 } from './csf';
+import { ReporterAPI } from './reporter-api';
 
 export function picky<T extends Record<string, any>, K extends keyof T>(
   obj: T,
@@ -253,12 +254,14 @@ export class StoryStore<TRenderer extends Renderer> {
   getStoryContext(story: PreparedStory<TRenderer>, { forceInitialArgs = false } = {}) {
     const userGlobals = this.userGlobals.get();
     const { initialGlobals } = this.userGlobals;
+    const reporting = new ReporterAPI();
     return prepareContext({
       ...story,
       args: forceInitialArgs ? story.initialArgs : this.args.get(story.id),
       initialGlobals,
       globalTypes: this.projectAnnotations.globalTypes,
       userGlobals,
+      reporting,
       globals: {
         ...userGlobals,
         ...story.storyGlobals,
diff --git a/code/core/src/preview-api/modules/store/csf/portable-stories.ts b/code/core/src/preview-api/modules/store/csf/portable-stories.ts
index b9efd7da979..a2971bcf5db 100644
--- a/code/core/src/preview-api/modules/store/csf/portable-stories.ts
+++ b/code/core/src/preview-api/modules/store/csf/portable-stories.ts
@@ -26,6 +26,7 @@ import { MountMustBeDestructuredError } from '@storybook/core/preview-errors';
 import { dedent } from 'ts-dedent';
 
 import { HooksContext } from '../../../addons';
+import { ReporterAPI } from '../reporter-api';
 import { composeConfigs } from './composeConfigs';
 import { getValuesFromArgTypes } from './getValuesFromArgTypes';
 import { normalizeComponentAnnotations } from './normalizeComponentAnnotations';
@@ -146,12 +147,15 @@ export function composeStory<TRenderer extends Renderer = Renderer, TArgs extend
     ...story.storyGlobals,
   };
 
+  const reporting = new ReporterAPI();
+
   const initializeContext = () => {
     const context: StoryContext<TRenderer> = prepareContext({
       hooks: new HooksContext(),
       globals,
       args: { ...story.initialArgs },
       viewMode: 'story',
+      reporting,
       loaded: {},
       abortSignal: new AbortController().signal,
       step: (label, play) => story.runStep(label, play, context),
@@ -258,6 +262,7 @@ export function composeStory<TRenderer extends Renderer = Renderer, TArgs extend
       argTypes: story.argTypes as StrictArgTypes<TArgs>,
       play: playFunction!,
       run,
+      reporting,
       tags: story.tags,
     }
   );
diff --git a/code/core/src/preview-api/modules/store/index.ts b/code/core/src/preview-api/modules/store/index.ts
index f6694ad9017..ea16e35bc90 100644
--- a/code/core/src/preview-api/modules/store/index.ts
+++ b/code/core/src/preview-api/modules/store/index.ts
@@ -10,3 +10,4 @@ export * from './decorators';
 export * from './args';
 export * from './autoTitle';
 export * from './sortStories';
+export * from './reporter-api';
diff --git a/code/core/src/preview-api/modules/store/reporter-api.ts b/code/core/src/preview-api/modules/store/reporter-api.ts
new file mode 100644
index 00000000000..dc2acc34d1d
--- /dev/null
+++ b/code/core/src/preview-api/modules/store/reporter-api.ts
@@ -0,0 +1,14 @@
+export interface Report {
+  id: string;
+  version: number;
+  result: unknown;
+  status: 'failed' | 'passed' | 'warning';
+}
+
+export class ReporterAPI {
+  reports: Report[] = [];
+
+  async addReport(report: Report) {
+    this.reports.push(report);
+  }
+}
diff --git a/code/core/src/types/modules/composedStory.ts b/code/core/src/types/modules/composedStory.ts
index 7f8d52add05..c5a58b280cd 100644
--- a/code/core/src/types/modules/composedStory.ts
+++ b/code/core/src/types/modules/composedStory.ts
@@ -9,6 +9,7 @@ import type {
   Tag,
 } from '@storybook/csf';
 
+import type { ReporterAPI } from '../../preview-api';
 import type {
   AnnotatedStoryFn,
   Args,
@@ -49,6 +50,7 @@ export type ComposedStoryFn<
   storyName: string;
   parameters: Parameters;
   argTypes: StrictArgTypes<TArgs>;
+  reporting: ReporterAPI;
   tags: Tag[];
   globals: Globals;
 };