diff --git a/.changeset/bright-patterns-neab.md b/.changeset/bright-patterns-neab.md
new file mode 100644
index 0000000000..74a5ccbf1b
--- /dev/null
+++ b/.changeset/bright-patterns-neab.md
@@ -0,0 +1,8 @@
+---
+"@hyperdx/app": minor
+"@hyperdx/api": minor
+---
+
+feat: add event patterns as a first-class dashboard tile type
+
+Event patterns can now be created, edited, and saved as dashboard tiles with a dedicated "Pattern Expression" editor. Supported across the UI, MCP server, and External API v2.
diff --git a/packages/api/openapi.json b/packages/api/openapi.json
index 7d938b5c14..00605a92c1 100644
--- a/packages/api/openapi.json
+++ b/packages/api/openapi.json
@@ -1983,6 +1983,47 @@
}
}
},
+ "EventPatternsChartConfig": {
+ "type": "object",
+ "required": [
+ "displayType",
+ "sourceId"
+ ],
+ "description": "Configuration for an event pattern mining tile. Clusters log or trace events by recurring message shapes.",
+ "properties": {
+ "displayType": {
+ "type": "string",
+ "enum": [
+ "event_patterns"
+ ],
+ "description": "Display type discriminator. Must be \"event_patterns\" for pattern mining tiles.",
+ "example": "event_patterns"
+ },
+ "sourceId": {
+ "type": "string",
+ "description": "ID of the data source to mine patterns from.",
+ "example": "65f5e4a3b9e77c001a111111"
+ },
+ "select": {
+ "type": "string",
+ "maxLength": 10000,
+ "description": "Column or expression to mine patterns from. Leave empty to use the source default (Body for logs, SpanName for traces).",
+ "default": "",
+ "example": "Body"
+ },
+ "where": {
+ "type": "string",
+ "maxLength": 10000,
+ "description": "Filter condition for the pattern mining query (syntax depends on whereLanguage).",
+ "default": "",
+ "example": "level:error"
+ },
+ "whereLanguage": {
+ "$ref": "#/components/schemas/QueryLanguage",
+ "description": "Query language for the where clause."
+ }
+ }
+ },
"MarkdownChartConfig": {
"type": "object",
"required": [
@@ -2495,7 +2536,7 @@
}
},
"TileConfig": {
- "description": "Tile chart configuration. displayType is the primary discriminant and determines which variant group applies. For displayTypes that support both builder and Raw SQL modes (line, stacked_bar, table, number, pie), configType is the secondary discriminant: omit it for the builder variant or set it to \"sql\" for the Raw SQL variant. The heatmap, search, and markdown displayTypes only have a builder variant.\n",
+ "description": "Tile chart configuration. displayType is the primary discriminant and determines which variant group applies. For displayTypes that support both builder and Raw SQL modes (line, stacked_bar, table, number, pie), configType is the secondary discriminant: omit it for the builder variant or set it to \"sql\" for the Raw SQL variant. The heatmap, search, event_patterns, and markdown displayTypes only have a builder variant.\n",
"oneOf": [
{
"$ref": "#/components/schemas/LineChartConfig"
@@ -2518,6 +2559,9 @@
{
"$ref": "#/components/schemas/SearchChartConfig"
},
+ {
+ "$ref": "#/components/schemas/EventPatternsChartConfig"
+ },
{
"$ref": "#/components/schemas/MarkdownChartConfig"
}
@@ -2532,6 +2576,7 @@
"pie": "#/components/schemas/PieChartConfig",
"heatmap": "#/components/schemas/HeatmapChartConfig",
"search": "#/components/schemas/SearchChartConfig",
+ "event_patterns": "#/components/schemas/EventPatternsChartConfig",
"markdown": "#/components/schemas/MarkdownChartConfig"
}
}
diff --git a/packages/api/src/mcp/tools/dashboards/schemas.ts b/packages/api/src/mcp/tools/dashboards/schemas.ts
index 54ecd6d867..640aabb243 100644
--- a/packages/api/src/mcp/tools/dashboards/schemas.ts
+++ b/packages/api/src/mcp/tools/dashboards/schemas.ts
@@ -716,6 +716,31 @@ const mcpSearchTileSchema = mcpTileLayoutSchema.extend({
}),
});
+const mcpEventPatternsTileSchema = mcpTileLayoutSchema.extend({
+ config: z.object({
+ displayType: z
+ .literal('event_patterns')
+ .describe('Event pattern mining tile'),
+ sourceId: z.string().describe('Source ID – call clickstack_list_sources'),
+ where: z
+ .string()
+ .optional()
+ .default('')
+ .describe('Filter in Lucene syntax. Example: "level:error"'),
+ whereLanguage:
+ SearchConditionTrimmedLanguageSchema.optional().default('lucene'),
+ select: z
+ .string()
+ .optional()
+ .default('')
+ .describe(
+ 'Pattern expression — column or expression to mine patterns from. ' +
+ 'Leave empty to use the source default (Body for logs, SpanName for traces). ' +
+ 'Example: "Body", "SpanName", "SpanAttributes[\'http.url\']"',
+ ),
+ }),
+});
+
const mcpMarkdownTileSchema = mcpTileLayoutSchema.extend({
config: z.object({
displayType: z.literal('markdown').describe('Free-form Markdown text tile'),
@@ -811,6 +836,7 @@ const mcpTileSchema = z.union([
mcpPieTileSchema,
mcpHeatmapTileSchema,
mcpSearchTileSchema,
+ mcpEventPatternsTileSchema,
mcpMarkdownTileSchema,
mcpSqlTileSchema,
]);
@@ -849,6 +875,9 @@ const mcpPatchTileSchema = z.union([
mcpPatchTileLayoutSchema.extend({
config: mcpSearchTileSchema.shape.config,
}),
+ mcpPatchTileLayoutSchema.extend({
+ config: mcpEventPatternsTileSchema.shape.config,
+ }),
mcpPatchTileLayoutSchema.extend({
config: mcpMarkdownTileSchema.shape.config,
}),
diff --git a/packages/api/src/mcp/tools/query/helpers.ts b/packages/api/src/mcp/tools/query/helpers.ts
index 5a148a703e..aa6cf77a4a 100644
--- a/packages/api/src/mcp/tools/query/helpers.ts
+++ b/packages/api/src/mcp/tools/query/helpers.ts
@@ -33,6 +33,8 @@ import { trimToolResponse } from '@/utils/trimToolResponse';
import type { ExternalDashboardTileWithId } from '@/utils/zod';
import { externalDashboardTileSchemaWithId } from '@/utils/zod';
+import { runEventPatterns } from './runEventPatterns';
+
// ─── Source body expression helpers ──────────────────────────────────────────
export interface SourceBodyFields {
@@ -404,6 +406,26 @@ export async function runConfigTile(
};
}
+ // Event-patterns tiles need the Drain pattern-mining pipeline, not the
+ // generic chart-config SQL renderer. Delegate to the same function the
+ // standalone clickstack_event_patterns tool uses.
+ if (builderConfig.displayType === DisplayType.EventPatterns) {
+ const selectStr =
+ typeof builderConfig.select === 'string' ? builderConfig.select : '';
+ return runEventPatterns(
+ teamId,
+ builderConfig.source,
+ startDate,
+ endDate,
+ {
+ where: builderConfig.where ?? '',
+ whereLanguage:
+ (builderConfig.whereLanguage as 'lucene' | 'sql') ?? 'lucene',
+ bodyExpression: selectStr || undefined,
+ },
+ );
+ }
+
const source = await getSource(teamId, builderConfig.source);
if (!source) {
return mcpUserError(
diff --git a/packages/api/src/routers/external-api/__tests__/dashboards.test.ts b/packages/api/src/routers/external-api/__tests__/dashboards.test.ts
index 079a1273aa..0a7777d3b6 100644
--- a/packages/api/src/routers/external-api/__tests__/dashboards.test.ts
+++ b/packages/api/src/routers/external-api/__tests__/dashboards.test.ts
@@ -2413,6 +2413,21 @@ describe('External API v2 Dashboards - new format', () => {
},
};
+ const eventPatternsChart: ExternalDashboardTile = {
+ name: 'Event Patterns',
+ x: 18,
+ y: 3,
+ w: 6,
+ h: 3,
+ config: {
+ displayType: 'event_patterns',
+ sourceId: traceSource._id.toString(),
+ select: 'SpanName',
+ where: 'level:error',
+ whereLanguage: 'lucene',
+ },
+ };
+
// Act
const response = await authRequest('post', BASE_URL)
.send({
@@ -2425,6 +2440,7 @@ describe('External API v2 Dashboards - new format', () => {
markdownChart,
pieChart,
heatmapChart,
+ eventPatternsChart,
],
tags: ['round-trip-test'],
})
@@ -2438,6 +2454,9 @@ describe('External API v2 Dashboards - new format', () => {
expect(omit(response.body.data.tiles[4], ['id'])).toEqual(markdownChart);
expect(omit(response.body.data.tiles[5], ['id'])).toEqual(pieChart);
expect(omit(response.body.data.tiles[6], ['id'])).toEqual(heatmapChart);
+ expect(omit(response.body.data.tiles[7], ['id'])).toEqual(
+ eventPatternsChart,
+ );
});
// Schema-level rejections that exercise pure Zod constraints
@@ -3976,6 +3995,22 @@ describe('External API v2 Dashboards - new format', () => {
},
};
+ const eventPatternsChart: ExternalDashboardTileWithId = {
+ id: new ObjectId().toString(),
+ name: 'Event Patterns',
+ x: 12,
+ y: 3,
+ w: 6,
+ h: 3,
+ config: {
+ displayType: 'event_patterns',
+ sourceId: traceSource._id.toString(),
+ select: 'SpanName',
+ where: 'level:error',
+ whereLanguage: 'lucene',
+ },
+ };
+
// Create an initial dashboard to update
const initialDashboard = await createTestDashboard();
@@ -3993,6 +4028,7 @@ describe('External API v2 Dashboards - new format', () => {
numberChart,
markdownChart,
heatmapChart,
+ eventPatternsChart,
],
tags: ['round-trip-test'],
})
@@ -4017,6 +4053,9 @@ describe('External API v2 Dashboards - new format', () => {
expect(omit(response.body.data.tiles[5], ['id'])).toEqual(
omit(heatmapChart, ['id']),
);
+ expect(omit(response.body.data.tiles[6], ['id'])).toEqual(
+ omit(eventPatternsChart, ['id']),
+ );
});
it('can round-trip all raw SQL chart config types', async () => {
diff --git a/packages/api/src/routers/external-api/v2/dashboards.ts b/packages/api/src/routers/external-api/v2/dashboards.ts
index e812a916d9..9e6d4686f5 100644
--- a/packages/api/src/routers/external-api/v2/dashboards.ts
+++ b/packages/api/src/routers/external-api/v2/dashboards.ts
@@ -922,6 +922,38 @@ const EXTERNAL_DASHBOARD_PROJECTION = {
* $ref: '#/components/schemas/QueryLanguage'
* description: Query language for the where clause.
*
+ * EventPatternsChartConfig:
+ * type: object
+ * required:
+ * - displayType
+ * - sourceId
+ * description: Configuration for an event pattern mining tile. Clusters log or trace events by recurring message shapes.
+ * properties:
+ * displayType:
+ * type: string
+ * enum: [event_patterns]
+ * description: Display type discriminator. Must be "event_patterns" for pattern mining tiles.
+ * example: "event_patterns"
+ * sourceId:
+ * type: string
+ * description: ID of the data source to mine patterns from.
+ * example: "65f5e4a3b9e77c001a111111"
+ * select:
+ * type: string
+ * maxLength: 10000
+ * description: Column or expression to mine patterns from. Leave empty to use the source default (Body for logs, SpanName for traces).
+ * default: ""
+ * example: "Body"
+ * where:
+ * type: string
+ * maxLength: 10000
+ * description: Filter condition for the pattern mining query (syntax depends on whereLanguage).
+ * default: ""
+ * example: "level:error"
+ * whereLanguage:
+ * $ref: '#/components/schemas/QueryLanguage'
+ * description: Query language for the where clause.
+ *
* MarkdownChartConfig:
* type: object
* required:
@@ -1299,7 +1331,7 @@ const EXTERNAL_DASHBOARD_PROJECTION = {
* both builder and Raw SQL modes (line, stacked_bar, table, number, pie),
* configType is the secondary discriminant: omit it for the builder
* variant or set it to "sql" for the Raw SQL variant. The heatmap,
- * search, and markdown displayTypes only have a builder variant.
+ * search, event_patterns, and markdown displayTypes only have a builder variant.
* oneOf:
* - $ref: '#/components/schemas/LineChartConfig'
* - $ref: '#/components/schemas/BarChartConfig'
@@ -1308,6 +1340,7 @@ const EXTERNAL_DASHBOARD_PROJECTION = {
* - $ref: '#/components/schemas/PieChartConfig'
* - $ref: '#/components/schemas/HeatmapChartConfig'
* - $ref: '#/components/schemas/SearchChartConfig'
+ * - $ref: '#/components/schemas/EventPatternsChartConfig'
* - $ref: '#/components/schemas/MarkdownChartConfig'
* discriminator:
* propertyName: displayType
@@ -1319,6 +1352,7 @@ const EXTERNAL_DASHBOARD_PROJECTION = {
* pie: '#/components/schemas/PieChartConfig'
* heatmap: '#/components/schemas/HeatmapChartConfig'
* search: '#/components/schemas/SearchChartConfig'
+ * event_patterns: '#/components/schemas/EventPatternsChartConfig'
* markdown: '#/components/schemas/MarkdownChartConfig'
*
* DashboardContainerTab:
diff --git a/packages/api/src/routers/external-api/v2/utils/dashboards.ts b/packages/api/src/routers/external-api/v2/utils/dashboards.ts
index 08b952ed2d..fe562ea411 100644
--- a/packages/api/src/routers/external-api/v2/utils/dashboards.ts
+++ b/packages/api/src/routers/external-api/v2/utils/dashboards.ts
@@ -247,6 +247,7 @@ const convertToExternalTileChartConfig = (
case DisplayType.Search:
case DisplayType.Markdown:
case DisplayType.Heatmap:
+ case DisplayType.EventPatterns:
logger.error(
{ config },
'Error converting chart config to external chart - unsupported display type for raw SQL config',
@@ -420,6 +421,14 @@ const convertToExternalTileChartConfig = (
numberFormat: config.numberFormat,
};
}
+ case DisplayType.EventPatterns:
+ return {
+ displayType: config.displayType,
+ sourceId,
+ select: stringValueOrDefault(config.select, ''),
+ where: stringValueOrDefault(config.where, ''),
+ whereLanguage: config.whereLanguage ?? 'lucene',
+ };
case undefined:
logger.error(
{ config },
@@ -788,6 +797,15 @@ export function convertToInternalTileConfig(
whereLanguage: externalConfig.whereLanguage ?? 'lucene',
} satisfies BuilderSavedChartConfig;
break;
+ case 'event_patterns':
+ internalConfig = {
+ ...pick(externalConfig, ['select', 'where']),
+ displayType: DisplayType.EventPatterns,
+ source: externalConfig.sourceId,
+ name,
+ whereLanguage: externalConfig.whereLanguage ?? 'lucene',
+ } satisfies BuilderSavedChartConfig;
+ break;
case 'markdown':
internalConfig = {
displayType: DisplayType.Markdown,
diff --git a/packages/api/src/utils/zod.ts b/packages/api/src/utils/zod.ts
index 5dd1798299..af3e5da236 100644
--- a/packages/api/src/utils/zod.ts
+++ b/packages/api/src/utils/zod.ts
@@ -424,6 +424,14 @@ const externalDashboardMarkdownChartConfigSchema = z.object({
markdown: z.string().max(50000).optional(),
});
+const externalDashboardEventPatternsChartConfigSchema = z.object({
+ displayType: z.literal('event_patterns'),
+ sourceId: objectIdSchema,
+ select: z.string().max(10000).optional().default(''),
+ where: z.string().max(10000).optional().default(''),
+ whereLanguage: whereLanguageSchema,
+});
+
const externalDashboardBuilderTileConfigSchema = z.discriminatedUnion(
'displayType',
[
@@ -435,6 +443,7 @@ const externalDashboardBuilderTileConfigSchema = z.discriminatedUnion(
externalDashboardHeatmapChartConfigSchema,
externalDashboardMarkdownChartConfigSchema,
externalDashboardSearchChartConfigSchema,
+ externalDashboardEventPatternsChartConfigSchema,
],
);
diff --git a/packages/app/src/DBDashboardPage.tsx b/packages/app/src/DBDashboardPage.tsx
index 138b04c963..fee6f62e30 100644
--- a/packages/app/src/DBDashboardPage.tsx
+++ b/packages/app/src/DBDashboardPage.tsx
@@ -145,6 +145,7 @@ import DBHeatmapChart, {
import { DBPieChart } from './components/DBPieChart';
import DBSqlRowTableWithSideBar from './components/DBSqlRowTableWithSidebar';
import OnboardingModal from './components/OnboardingModal';
+import PatternTable from './components/PatternTable';
import SearchWhereInput, {
getStoredLanguage,
} from './components/SearchInput/SearchWhereInput';
@@ -171,7 +172,9 @@ import {
import HDXMarkdownChart from './HDXMarkdownChart';
import { withAppNav } from './layout';
import {
+ getEventBody,
getFirstTimestampValueExpression,
+ isSingleExpression,
useSource,
useSources,
} from './source';
@@ -1179,6 +1182,57 @@ const Tile = forwardRef(
/>
)}
+ {effectiveQueriedConfig?.displayType ===
+ DisplayType.EventPatterns &&
+ isBuilderChartConfig(effectiveQueriedConfig) &&
+ isBuilderSavedChartConfig(chart.config) && (
+