static/app/components/seer/markdown/seerMarkdown.mdx
import * as Storybook from 'sentry/stories';
import { BasicDemo, EmbedRegistry, LinkifyDemo, StreamingEmbedDemo, } from './stories/components';
<SeerMarkdown> wraps the core <Markdown> component with Seer-specific overrides: issue short ID linkification, origin-relative links, flattened headings, and an embed system that renders structured widgets inline from tag syntax.
import {SeerMarkdown} from 'sentry/components/seer/markdown';
<SeerMarkdown raw={seerOutput} />;
Renders standard markdown with Seer's component overrides and opinionated embeds applied.
<Storybook.Demo> <BasicDemo /> </Storybook.Demo>
<SeerMarkdown raw={markdownString} />
Embeds are rich widgets rendered inline from Markdoc-style tag syntax. The Seer agent produces these tags; the frontend resolves them through a registry + schema system.
{% timestamp %}{"value":"2025-07-15T14:30:00Z","format":"relative"}{% /timestamp %}
The JSON body between the opening and closing tags is the embed's data payload. It is validated against a Zod schema before rendering — invalid data renders nothing (with a dev-mode console warning).
The embed system has four parts:
| File | Role |
|---|---|
embeds/schemas.ts | Zod schemas + metadata (description, level, feature flag) per embed |
embeds/utils.tsx | defineSeerEmbed() factory — creates components with automatic Zod validation |
embeds/registry.tsx | Runtime Map of name → component; queried by the Tag override |
embeds/index.ts | Imports all embed components and registers them |
Data flow:
{% name %}{ ... }{% /name %} tagsmarked lexer (with tag extensions) tokenizes them as TagTokensSeerMarkdown's Tag override looks up the name in SeerEmbedRegistrydata against its Zod schema and rendersBackend codegen: Schemas are exported to JSON Schema via pnpm gen:embed-widgets, producing src/sentry/seer/agent/embed_widgets.generated.json. The backend sends these schemas to the Seer agent so it knows what embeds are available and how to format them.
Adding a new embed requires three files and a codegen step. Use the /seer-embed skill for guided scaffolding.
1. Add the schema in embeds/schemas.ts:
export const SEER_EMBED_SCHEMAS = {
// ...existing schemas
myWidget: {
description: 'A short description for the LLM.',
level: ['block'],
schema: z.object({
title: z.string(),
value: z.number(),
}),
examples: [{label: 'Basic', data: {title: 'Errors', value: 42}}],
},
} as const satisfies Record<string, SeerEmbedSchema>;
2. Create the component in embeds/components/myWidget.tsx:
import {defineSeerEmbed} from 'sentry/components/seer/markdown/embeds/utils';
export const MyWidget = defineSeerEmbed({
name: 'myWidget',
render({title, value}) {
return (
<span>
{title}: {value}
</span>
);
},
});
3. Register it in embeds/index.ts:
import {MyWidget} from './components/myWidget';
const embeds = [Timestamp, MyWidget];
4. Regenerate the backend schema:
pnpm gen:embed-widgets
| Prop | Type | Default | Description |
|---|---|---|---|
raw | string | — | The markdown string to render |
variant | 'static' | 'streaming' | 'static' | Streaming mode enables decode animation and suppresses partial tags |
All other Markdown props are supported except components, which is pre-configured.