diff --git a/apps/sim/app/workspace/[workspaceId]/changelog/changelog.tsx b/apps/sim/app/workspace/[workspaceId]/changelog/changelog.tsx
index faf3f8857d1..48bda52d9da 100644
--- a/apps/sim/app/workspace/[workspaceId]/changelog/changelog.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/changelog/changelog.tsx
@@ -3,13 +3,13 @@
import { useState } from 'react'
import { Chip, cn, scrollFadeAttributes, scrollFadeClass, useScrollEdges } from '@sim/emcn'
import { Rss } from '@sim/emcn/icons'
-import { EmptyState } from '@/components/empty-state/empty-state'
import type { ChangelogRelease } from '@/lib/api/contracts/changelog'
import {
ReleaseDetailsModal,
ReleaseEntry,
} from '@/app/workspace/[workspaceId]/changelog/components'
import ChangelogLoading from '@/app/workspace/[workspaceId]/changelog/loading'
+import { ChangelogEmptyState } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state'
import { Resource } from '@/app/workspace/[workspaceId]/components/resource/resource'
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
import { PermissionAccessBoundary } from '@/ee/access-requests/components/permission-access-boundary'
@@ -47,10 +47,7 @@ function ChangelogContent({ workspaceId }: ChangelogProps) {
query.refetch()}>Retry
) : releases.length === 0 ? (
-
+
) : (
{
+ const y = release.top + 26 + index * NOTE_PITCH
+ return (
+
+
+
+
+ )
+ })
+}
+
+/** A release timeline: the latest release ringed at the head, earlier ones falling away below. */
+function ChangelogGraphic() {
+ const latestY = LATEST.top + 8
+ return (
+
+ )
+}
+
+/** Empty state for a workspace that has no published releases yet. */
+export function ChangelogEmptyState() {
+ return (
+
}
+ title='Changelog'
+ description='Sim publishes a release here when it ships changes to this workspace.'
+ />
+ )
+}
diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/index.ts b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/index.ts
index 488ce28e54e..cd37a146c47 100644
--- a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/index.ts
+++ b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/index.ts
@@ -1,3 +1,4 @@
+export { ChangelogEmptyState } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/changelog-empty-state'
export { DashboardEmptyState } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/dashboard-empty-state'
export { DocumentsEmptyState } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/documents-empty-state'
export { FilesEmptyState } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/files-empty-state'
@@ -5,3 +6,4 @@ export { KnowledgeEmptyState } from '@/app/workspace/[workspaceId]/components/re
export { LogsEmptyState } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/logs-empty-state'
export { ResourceNoResults } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/resource-no-results'
export { TablesEmptyState } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/tables-empty-state'
+export { TestsEmptyState } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/tests-empty-state'
diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/tests-empty-state.tsx b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/tests-empty-state.tsx
new file mode 100644
index 00000000000..946fa1cfc79
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/tests-empty-state.tsx
@@ -0,0 +1,125 @@
+import { cn } from '@sim/emcn'
+import { EmptyState } from '@/components/empty-state/empty-state'
+import { HAIRLINE } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/hairline'
+import { MASK_NO_REPEAT } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/mask'
+
+/**
+ * Skeleton ink — see the `INK` note in `tables-empty-state.tsx` for why not the surface
+ * ramp. `mark` is the tables selection ring's opaque mix, so a status glyph reads as
+ * drawn rather than as more placeholder.
+ */
+const INK = {
+ title: 'color-mix(in srgb, var(--text-secondary) 30%, transparent)',
+ detail: 'color-mix(in srgb, var(--text-secondary) 15%, transparent)',
+ mark: 'color-mix(in srgb, var(--text-secondary) 46%, var(--bg))',
+} as const
+
+/** The logs feed's falloff — a run report is the same repeating structure. */
+const REPORT_FADE =
+ '[-webkit-mask-image:linear-gradient(to_bottom,#000_44%,transparent_100%)] [mask-image:linear-gradient(to_bottom,#000_44%,transparent_100%)]'
+
+type TestStatus = 'pass' | 'fail'
+
+interface ReportRow {
+ status: TestStatus
+ title: number
+ detail: number
+}
+
+/**
+ * The failure sits second, inside the fade's opaque head, so the one row held in a card
+ * never dissolves mid-stroke.
+ */
+const ROWS: readonly ReportRow[] = [
+ { status: 'pass', title: 72, detail: 110 },
+ { status: 'fail', title: 64, detail: 124 },
+ { status: 'pass', title: 80, detail: 96 },
+ { status: 'pass', title: 58, detail: 116 },
+] as const
+
+const ROW_PITCH = 38
+const BADGE_RADIUS = 8
+const GLYPH = BADGE_RADIUS * 0.42
+
+const MARK_STROKE = {
+ stroke: INK.mark,
+ strokeWidth: 1.4,
+ strokeLinecap: 'round',
+ strokeLinejoin: 'round',
+} as const
+
+interface StatusBadgeProps {
+ cx: number
+ cy: number
+ status: TestStatus
+}
+
+/** A pass ticks on a hairline ring; a fail crosses on the heavier ring it shares with its card. */
+function StatusBadge({ cx, cy, status }: StatusBadgeProps) {
+ const glyph =
+ status === 'pass'
+ ? `M ${cx - GLYPH} ${cy} L ${cx - GLYPH * 0.25} ${cy + GLYPH * 0.75} L ${cx + GLYPH} ${cy - GLYPH * 0.7}`
+ : `M ${cx - GLYPH * 0.75} ${cy - GLYPH * 0.75} L ${cx + GLYPH * 0.75} ${cy + GLYPH * 0.75} M ${cx + GLYPH * 0.75} ${cy - GLYPH * 0.75} L ${cx - GLYPH * 0.75} ${cy + GLYPH * 0.75}`
+ return (
+
+
+
+
+ )
+}
+
+/** A test run report: passing rows, the one failure held in a card like the logs feed's latest run. */
+function TestsGraphic() {
+ return (
+
+ )
+}
+
+/** Empty state for a workspace with no tests yet. */
+export function TestsEmptyState() {
+ return (
+
}
+ title='Tests'
+ description='Write tests to check your workflows behave before you deploy.'
+ />
+ )
+}
diff --git a/apps/sim/app/workspace/[workspaceId]/tests/tests.tsx b/apps/sim/app/workspace/[workspaceId]/tests/tests.tsx
index 4960ec0ba57..bcdb1450070 100644
--- a/apps/sim/app/workspace/[workspaceId]/tests/tests.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/tests/tests.tsx
@@ -8,7 +8,10 @@ import { useQueryStates } from 'nuqs'
import type { WorkflowTestRecord } from '@/lib/api/contracts/workflow-tests'
import { SEARCH_DEBOUNCE_MS } from '@/lib/url-state'
import { ownerCell } from '@/app/workspace/[workspaceId]/components/resource/components/owner-cell'
-import { ResourceNoResults } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state'
+import {
+ ResourceNoResults,
+ TestsEmptyState,
+} from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state'
import type {
SearchConfig,
SortConfig,
@@ -154,6 +157,8 @@ export function Tests({ workspaceId }: TestsProps) {
{query.error.message}
query.refetch()}>Retry
+ ) : query.data?.length === 0 ? (
+