v0.8.12: memory improvements, cli expansion, semrush, microsoft word, perf improvements

This commit is contained in:
Waleed
2026-08-26 13:58:32 -07:00
committed by GitHub
1552 changed files with 212680 additions and 21148 deletions
+6 -9
View File
@@ -1,6 +1,6 @@
---
name: babysit
description: Drive a PR to a clean review (Greptile 5/5, zero open threads) — ships if needed, keeps it mergeable against staging, triggers Greptile/Cursor Bugbot, fixes real findings, replies to and resolves every thread, and loops until clean
description: Drive a PR to a clean review (Greptile 5/5, zero open threads) — ships if needed, keeps it mergeable against staging, triggers Greptile, fixes real findings, replies to and resolves every thread, and loops until clean
---
# Babysit PRs
@@ -58,9 +58,9 @@ round. Always check both conditions freshly after every push.
2. **If the PR has a merge conflict**, merge `origin/staging`, resolve the conflicts, run the
usual pre-push checks, push, and go to step 8 to re-trigger review.
3. **If no review has run yet** (fresh PR, no Greptile/Cursor comments): they usually run
automatically on PR open — confirm via `gh pr checks <n>` (look for `Cursor Bugbot` /
`Greptile Review`) and wait for that first round before doing anything else.
3. **If no review has run yet** (fresh PR, no Greptile comments): Greptile usually runs
automatically on PR open — confirm via `gh pr checks <n>` (look for `Greptile Review`) and
wait for that first round before doing anything else.
4. **If a review round has landed and it isn't clean**: for every thread where
`isResolved: false`, triage the finding on its own merits — this is the part that requires
@@ -113,15 +113,13 @@ round. Always check both conditions freshly after every push.
rounds; checking sync only before the push (step 6) and never after is how a bad push or a
PR whose commit history quietly went stale between rounds goes unnoticed.
8. **Re-trigger review** by posting `@greptile` and `@cursor review` as **two separate PR
comments** — never combine them into one comment, each bot only responds to its own mention:
8. **Re-trigger review** by posting `@greptile` as its own PR comment:
```bash
gh pr comment <n> --body "@greptile"
gh pr comment <n> --body "@cursor review"
```
9. **Wait for the new round**, then go back to step 1. Pace the wait with `ScheduleWakeup` using
a fallback delay of ~250300s (Greptile/Cursor typically take 13 minutes) — never busy-poll
a fallback delay of ~250300s (Greptile typically takes 13 minutes) — never busy-poll
in a sleep loop. Pass the same `/loop babysit PR <n>` prompt on each wakeup so the loop
resumes correctly.
@@ -147,7 +145,6 @@ notification email.
## Hard rules
- Never post the two re-review mentions as a single combined comment.
- Never paste prod evidence into a reply without scrubbing it first (see above).
- Never resolve a thread without replying to it first.
- Never fix a finding with a hacky workaround — if the clean fix isn't obvious, find the sibling
+3 -1
View File
@@ -113,7 +113,9 @@ Every paged list's binding is declared in `lib/api/contracts/v2/__tests__/list-p
Return `nextCursor: null` on the last page and only then. Never construct a cursor client-side.
**Ordering is `sortBy` + `sortOrder`, except where there is nothing to sort by.** Fourteen lists take the pair. Two — `GET /logs` and `GET /workflows/{id}/runs` — have exactly one sortable column (start time), so there is no `sortBy` to pair with and the direction rides on a single `order` param; `sortBy`/`sortOrder` are not accepted there. That split is documented in both contracts and is the *only* sanctioned deviation. A new list picks the pair. Do not "fix" the two by accepting `sortOrder` as an alias: an alias is a second spelling of one thing with undefined precedence when both arrive, which is its own inconsistency, and renaming `order` would break every shipped caller.
**Ordering is `sortBy` + `sortOrder`, except where there is nothing to sort by.** Nearly every paged list takes the pair; `CURSOR_BINDINGS` in `contracts/v2/__tests__/list-pagination.test.ts` is the authoritative set. Exactly one — `GET /workflows/{workflowId}/runs` — has a single sortable column (start time), so there is no `sortBy` to pair with and the direction rides on a single `order` param; `sortBy`/`sortOrder` are not accepted there. That is the *only* sanctioned deviation, and it is documented in its contract. A new list picks the pair. Do not "fix" it by accepting `sortOrder` as an alias: an alias is a second spelling of one thing with undefined precedence when both arrive, which is its own inconsistency.
`GET /logs` was the second exception until it absorbed `POST /logs/query`. That fold is the cautionary tale for this rule: the justification for the `order` spelling was "logs have exactly one sortable column", and a second endpoint sorting the same rows four ways had already disproved it. When a rule's premise is contradicted by another endpoint on the same collection, fix the premise rather than documenting the exception.
**A boolean query param is a real boolean**, declared with `booleanQueryFlagSchema` from `contracts/primitives.ts`. It coerces `'true'`/`'1'` and `'false'`/`'0'`/`''`, so it is a strict widening of a `z.enum(['true','false'])` — which is what two v2 params used to be, purely by inheritance from the internal shapes they reused. Reusing an internal `.shape.x` inherits the internal spelling; re-declare instead when the internal one is not the v2 convention.
+1
View File
@@ -80,6 +80,7 @@ jobs:
echo "ERROR: db:push needs an interactive rename decision; land it as a versioned migration instead of relying on push." >&2
exit 1
fi
bun run ./scripts/apply-dev-workspace-file-size-cutover.ts
else
echo "Applying versioned migrations (db:migrate)"
bun run ./scripts/migrate.ts
+13
View File
@@ -110,5 +110,18 @@ test.describe('desktop shell smoke', () => {
const window = await app.firstWindow()
await window.waitForSelector('#retry', { timeout: 30_000 })
expect(window.url().startsWith('file:')).toBe(true)
await expect(window.locator('.wordmark')).toBeVisible()
await expect(window.locator('.wordmark')).toHaveAttribute('aria-label', 'Sim')
await expect(window.locator('#title')).toHaveText('Cant connect to Sim')
await expect(window.locator('#status')).toHaveText('Check status')
await expect
.poll(() => window.evaluate(() => document.fonts.check('16px "Season Sans"')))
.toBe(true)
await expect(window.locator('#retry')).toHaveCSS('height', '30px')
await expect(window.locator('#retry')).toHaveCSS('border-radius', '8px')
await expect(window.locator('#retry')).toHaveCSS('padding-left', '8px')
await expect(window.locator('#retry')).toHaveCSS('font-size', '14px')
await expect(window.locator('#retry')).toHaveCSS('line-height', '20px')
await expect(window.locator('#retry')).toHaveCSS('text-align', 'left')
})
})
+4
View File
@@ -10,6 +10,10 @@ files:
- dist/**
- static/**
- package.json
- from: ../sim/public/brand/fonts
to: static
filter:
- SeasonSansUprightsVF.woff2
asar: true
+142 -81
View File
@@ -4,26 +4,27 @@
<meta charset="utf-8" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'"
content="default-src 'none'; font-src 'self'; style-src 'unsafe-inline'; script-src 'unsafe-inline'"
/>
<title>Sim Cant connect</title>
<title>Sim - Cant connect</title>
<style>
:root {
color-scheme: light dark;
--bg: #ffffff;
--fg: #0c0c0c;
--muted: #6b6b6b;
--border: #e4e4e4;
--accent: #701ffc;
--accent-fg: #ffffff;
@font-face {
font-family: 'Season Sans';
src:
url('../../sim/public/brand/fonts/SeasonSansUprightsVF.woff2') format('woff2'),
url('./SeasonSansUprightsVF.woff2') format('woff2');
font-style: normal;
font-weight: 300 800;
font-display: block;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #0c0c0c;
--fg: #f4f4f4;
--muted: #9a9a9a;
--border: #262626;
}
:root {
color-scheme: light;
--bg: #fefefe;
--text-primary: #1a1a1a;
--text-body: #434343;
--text-muted: #7a7a7a;
--text-inverse: #ffffff;
--surface-hover: #f2f2f2;
}
* {
box-sizing: border-box;
@@ -35,111 +36,171 @@
}
body {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
min-width: 320px;
padding-top: max(env(titlebar-area-height, 38px), 38px);
background: var(--bg);
color: var(--fg);
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
color: var(--text-primary);
font-family: 'Season Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
-webkit-font-smoothing: antialiased;
user-select: none;
}
main {
max-width: 420px;
padding: 32px;
text-align: center;
header {
flex: none;
width: 100%;
max-width: 1460px;
margin: 0 auto;
padding: 16px 80px;
-webkit-app-region: drag;
}
.mark {
width: 56px;
height: 56px;
margin: 0 auto 20px;
border-radius: 14px;
background: var(--accent);
.wordmark {
display: block;
width: 37px;
height: 18px;
margin: 6px 0;
color: var(--text-body);
}
main {
display: flex;
flex: 1;
width: 100%;
align-items: center;
justify-content: center;
color: var(--accent-fg);
font-size: 26px;
font-weight: 600;
letter-spacing: -0.02em;
padding: 0 16px 64px;
}
.content {
display: flex;
width: 100%;
max-width: 410px;
flex-direction: column;
align-items: center;
gap: 12px;
text-align: center;
}
h1 {
font-size: 17px;
font-weight: 600;
margin: 0 0 8px;
max-width: 100%;
margin: 0;
font-size: 40px;
font-weight: 400;
letter-spacing: -0.02em;
line-height: 1.1;
text-wrap: balance;
}
p {
font-size: 13px;
line-height: 1.5;
color: var(--muted);
margin: 0 0 24px;
max-width: 100%;
margin: 0;
color: var(--text-muted);
font-size: 18px;
line-height: 28px;
}
.actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: center;
margin-top: 12px;
}
button {
display: inline-flex;
height: 30px;
align-items: center;
gap: 6px;
appearance: none;
border: 1px solid var(--border);
border: 0;
background: transparent;
color: var(--fg);
color: var(--text-body);
border-radius: 8px;
padding: 7px 14px;
font-size: 13px;
font-weight: 500;
padding: 0 8px;
font: inherit;
font-size: 14px;
line-height: 20px;
text-align: left;
cursor: pointer;
outline: none;
transition:
color 150ms cubic-bezier(0.4, 0, 0.2, 1),
background-color 150ms cubic-bezier(0.4, 0, 0.2, 1),
border-color 150ms cubic-bezier(0.4, 0, 0.2, 1),
text-decoration-color 150ms cubic-bezier(0.4, 0, 0.2, 1),
fill 150ms cubic-bezier(0.4, 0, 0.2, 1),
stroke 150ms cubic-bezier(0.4, 0, 0.2, 1);
-webkit-app-region: no-drag;
}
button .label {
min-width: 0;
flex: 1;
overflow: hidden;
color: currentColor;
text-overflow: ellipsis;
white-space: nowrap;
}
button.primary {
background: var(--accent);
border-color: var(--accent);
color: var(--accent-fg);
background: var(--text-primary);
color: var(--text-inverse);
}
button:active {
opacity: 0.8;
}
.links {
margin-top: 20px;
font-size: 12px;
}
.links a {
color: var(--muted);
text-decoration: none;
margin: 0 8px;
cursor: pointer;
}
.links a:hover {
color: var(--fg);
@media (hover: hover) and (pointer: fine) {
button.primary:hover {
background: var(--text-body);
}
button.secondary:hover {
background: var(--surface-hover);
}
}
#detail {
font-size: 11px;
color: var(--muted);
margin-top: 16px;
min-height: 17px;
margin-top: 4px;
color: var(--text-muted);
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 11px;
line-height: 1.5;
}
@media (max-width: 720px) {
header {
padding-right: 32px;
padding-left: 32px;
}
}
@media (max-width: 480px) {
header {
padding-right: 20px;
padding-left: 20px;
}
h1 {
font-size: 34px;
}
}
</style>
</head>
<body>
<header aria-label="Sim">
<svg class="wordmark" viewBox="0 0 441 212" role="img" aria-label="Sim">
<g fill="currentColor">
<path d="M0 160.9H29.51C29.51 169.08 32.46 175.61 38.37 180.48C44.27 185.12 52.25 187.44 62.31 187.44C73.24 187.44 81.65 185.34 87.56 181.14C93.46 176.71 96.41 170.85 96.41 163.55C96.41 158.24 94.77 153.82 91.49 150.28C88.43 146.74 82.75 143.86 74.44 141.65L46.24 135.01C32.03 131.47 21.42 126.05 14.43 118.75C7.65 111.45 4.26 101.83 4.26 89.88C4.26 79.93 6.78 71.3 11.81 64C17.05 56.7 24.16 51.06 33.12 47.08C42.3 43.09 52.8 41.1 64.6 41.1C76.41 41.1 86.57 43.2 95.1 47.41C103.84 51.61 110.62 57.47 115.43 64.99C120.46 72.52 123.08 81.48 123.3 91.87H93.79C93.57 83.47 90.84 76.94 85.59 72.3C80.34 67.65 73.02 65.33 63.62 65.33C54 65.33 46.57 67.43 41.32 71.63C36.07 75.83 33.45 81.59 33.45 88.89C33.45 99.73 41.32 107.14 57.06 111.12L85.26 118.09C98.81 121.19 108.98 126.28 115.76 133.35C122.53 140.21 125.92 149.61 125.92 161.56C125.92 171.74 123.19 180.7 117.73 188.44C112.26 195.96 104.72 201.82 95.1 206.03C85.7 210.01 74.55 212 61.65 212C42.85 212 27.87 207.35 16.72 198.06C5.57 188.77 0 176.38 0 160.9Z" />
<path d="M232.8 212H202.13L202.13 49.76H229.54V77.39C232.8 68.34 239.11 60.66 247.81 54.7C256.73 48.52 267.5 45.43 280.12 45.43C294.26 45.43 306.01 49.29 315.36 57.02C324.72 64.75 330.81 75.01 333.64 87.82H328.09C330.27 75.01 336.25 64.75 346.04 57.02C355.83 49.29 367.9 45.43 382.26 45.43C400.54 45.43 414.89 50.84 425.34 61.66C435.78 72.47 441 87.26 441 106.03V212H410.98V113.65C410.98 100.84 407.71 91.02 401.19 84.17C394.88 77.11 386.29 73.58 375.41 73.58C367.79 73.58 361.05 75.34 355.17 78.88C349.52 82.19 345.06 87.04 341.8 93.45C338.53 99.85 336.9 107.36 336.9 115.97V212H306.55V113.32C306.55 100.51 303.4 90.8 297.09 84.17C290.78 77.33 282.19 73.91 271.31 73.91C263.69 73.91 256.95 75.67 251.08 79.21C245.42 82.52 240.96 87.38 237.7 93.78C234.43 99.96 232.8 107.36 232.8 115.97V212Z" />
<path d="M184.83 20.55C184.83 31.9 175.64 41.1 164.29 41.1C152.95 41.1 143.76 31.9 143.76 20.55C143.76 9.2 152.95 0 164.29 0C175.64 0 184.83 9.2 184.83 20.55Z" />
<path d="M179.43 212H149.16V49.76C153.76 51.91 158.88 53.12 164.29 53.12C169.7 53.12 174.83 51.91 179.43 49.76V212Z" />
</g>
</svg>
</header>
<main>
<div class="mark">S</div>
<h1 id="title">Cant connect to Sim</h1>
<p id="message">
Sim couldnt reach the server. Check your internet connection, then try again.
</p>
<div class="actions">
<button class="primary" id="retry">Retry</button>
<div class="content">
<h1 id="title">Cant connect to Sim</h1>
<p id="message">
Sim couldnt reach the server. Check your internet connection, then try again.
</p>
<div class="actions">
<button class="primary" id="retry"><span class="label">Retry</span></button>
<button class="secondary" id="status"><span class="label">Check status</span></button>
</div>
<div id="detail" role="status"></div>
</div>
<div class="links">
<a id="status">Check status</a>
</div>
<div id="detail"></div>
</main>
<script>
const params = new URLSearchParams(location.search)
const copy = {
offline: {
title: 'Youre offline',
message: 'Sim needs an internet connection. Reconnect, then try again — well also retry automatically.',
message: 'Sim needs an internet connection. Reconnect, then try again. Well also retry automatically.',
},
dns: {
title: 'Cant find the server',
+14 -11
View File
@@ -8,17 +8,20 @@ export const metadata = {
export default function NotFound() {
return (
<DocsPage>
<div className='flex min-h-[70vh] flex-col items-center justify-center gap-4 text-center'>
<h1 className='bg-gradient-to-b from-[var(--brand-accent)] to-[var(--brand-accent-hover)] bg-clip-text font-semibold text-8xl text-transparent'>
404
</h1>
<h2 className='font-semibold text-2xl text-[var(--text-primary)]'>Page Not Found</h2>
<p className='text-[var(--text-muted)]'>
The page you're looking for doesn't exist or has been moved.
</p>
<ChipLink href='/' variant='primary'>
Go home
</ChipLink>
<div className='flex min-h-[60vh] flex-col items-center justify-center px-4 py-24 text-center'>
<div className='flex w-full max-w-[410px] flex-col items-center gap-3'>
<h1 className='text-balance text-[40px] text-[var(--text-primary)] leading-[110%] tracking-[-0.02em]'>
Page not found
</h1>
<p className='text-[var(--text-muted)] text-lg'>
The page you&apos;re looking for doesn&apos;t exist or has been moved.
</p>
<div className='mt-3 flex flex-wrap items-center justify-center gap-2'>
<ChipLink href='/' variant='primary'>
Return home
</ChipLink>
</div>
</div>
</div>
</DocsPage>
)
+160
View File
@@ -4885,6 +4885,155 @@ export function MicrosoftOneDriveIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function MicrosoftWordIcon(props: SVGProps<SVGSVGElement>) {
const id = useId()
const bodyId = `word_body_${id}`
const midId = `word_mid_${id}`
const midGlowId = `word_mid_glow_${id}`
const midShadeId = `word_mid_shade_${id}`
const topId = `word_top_${id}`
const topGlowId = `word_top_glow_${id}`
const tileId = `word_tile_${id}`
const tileGlowId = `word_tile_glow_${id}`
const midPath =
'M5,15.04c0-2.49,2.01-4.5,4.5-4.5h20.39l5.11-2.54v12.5c0,1.93-1.57,3.5-3.5,3.5H11c-3.31,0-6,2.69-6,6v-14.96Z'
const topPath =
'M5,6C5,2.69,7.69,0,11,0h20.5c1.93,0,3.5,1.57,3.5,3.5v5c0,1.93-1.57,3.5-3.5,3.5H11c-3.31,0-6,2.69-6,6V6Z'
return (
<svg {...props} viewBox='0 0 35 36' xmlns='http://www.w3.org/2000/svg'>
<defs>
<radialGradient
id={bodyId}
cx='-619.29'
cy='488.84'
fx='-619.29'
fy='488.84'
r='1'
gradientTransform='translate(29495.74 9885.89) scale(47.57 -20.15)'
gradientUnits='userSpaceOnUse'
>
<stop offset='.18' stopColor='#1657f4' />
<stop offset='.57' stopColor='#0036c4' />
</radialGradient>
<linearGradient
id={midId}
x1='5'
y1='97'
x2='27.97'
y2='97'
gradientTransform='translate(0 116) scale(1 -1)'
gradientUnits='userSpaceOnUse'
>
<stop offset='0' stopColor='#66c0ff' />
<stop offset='.26' stopColor='#0094f0' />
</linearGradient>
<radialGradient
id={midGlowId}
cx='-637.72'
cy='517.98'
fx='-637.72'
fy='517.98'
r='1'
gradientTransform='translate(-40017.96 -12225.34) rotate(133.55) scale(29.36 -72.32)'
gradientUnits='userSpaceOnUse'
>
<stop offset='.14' stopColor='#d471ff' />
<stop offset='.83' stopColor='#509df5' stopOpacity='0' />
</radialGradient>
<radialGradient
id={midShadeId}
cx='-611.76'
cy='514.18'
fx='-611.76'
fy='514.18'
r='1'
gradientTransform='translate(-52234.57 11411.47) rotate(90) scale(18.62 -101.65)'
gradientUnits='userSpaceOnUse'
>
<stop offset='.28' stopColor='#4f006f' stopOpacity='0' />
<stop offset='1' stopColor='#4f006f' />
</radialGradient>
<linearGradient
id={topId}
x1='5'
y1='107.22'
x2='35'
y2='106.72'
gradientTransform='translate(0 116) scale(1 -1)'
gradientUnits='userSpaceOnUse'
>
<stop offset='0' stopColor='#9deaff' />
<stop offset='.2' stopColor='#3bd5ff' />
</linearGradient>
<radialGradient
id={topGlowId}
cx='-650.27'
cy='515.34'
fx='-650.27'
fy='515.34'
r='1'
gradientTransform='translate(-26921.47 -31089.42) rotate(166.85) scale(29.49 -70.64)'
gradientUnits='userSpaceOnUse'
>
<stop offset='.06' stopColor='#e4a7fe' />
<stop offset='.54' stopColor='#e4a7fe' stopOpacity='0' />
</radialGradient>
<radialGradient
id={tileId}
cx='-600.8'
cy='515.58'
fx='-600.8'
fy='515.58'
r='1'
gradientTransform='translate(1363.5 17878.99) rotate(45) scale(22.63 -22.63)'
gradientUnits='userSpaceOnUse'
>
<stop offset='.08' stopColor='#367af2' />
<stop offset='.87' stopColor='#001a8f' />
</radialGradient>
<radialGradient
id={tileGlowId}
cx='-598.04'
cy='557.24'
fx='-598.04'
fy='557.24'
r='1'
gradientTransform='translate(-7105.12 6724.6) rotate(90) scale(11.2 -12.77)'
gradientUnits='userSpaceOnUse'
>
<stop offset='.59' stopColor='#2763e5' stopOpacity='0' />
<stop offset='.97' stopColor='#58aafe' />
</radialGradient>
</defs>
<path
d='M5,27.09l14-17.09,16,11.11v11.39c0,1.93-1.57,3.5-3.5,3.5H11c-3.31,0-6-2.69-6-6v-2.91Z'
fill={`url(#${bodyId})`}
/>
<path d={midPath} fill={`url(#${midId})`} />
<path d={midPath} fill={`url(#${midGlowId})`} fillOpacity='.6' />
<path d={midPath} fill={`url(#${midShadeId})`} fillOpacity='.1' />
<path d={topPath} fill={`url(#${topId})`} />
<path d={topPath} fill={`url(#${topGlowId})`} fillOpacity='.8' />
<rect y='17' width='16' height='16' rx='3.25' ry='3.25' fill={`url(#${tileId})`} />
<rect
y='17'
width='16'
height='16'
rx='3.25'
ry='3.25'
fill={`url(#${tileGlowId})`}
fillOpacity='.65'
/>
<path
d='M13.49,20.43l-1.97,9.14h-2.35s-1.16-5.48-1.16-5.48l-1.22,5.49h-2.38l-1.89-9.14h1.94l1.17,6.03,1.16-6.03h2.38l1.21,6.03,1.14-6.03h1.97Z'
fill='#fff'
/>
</svg>
)
}
export function MicrosoftSharepointIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} fill='currentColor' viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg'>
@@ -6842,6 +6991,17 @@ export function AgiloftIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function SemrushIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 43.3 26'>
<path
fill='#FF642D'
d='M37.4,12.8c0,0.8-0.4,0.9-1.4,0.9c-1.1,0-1.3-0.2-1.4-1c-0.2-2.1-1.6-3.9-4-4c-0.7-0.1-0.9-0.3-0.9-1.3 c0-0.9,0.1-1.3,0.8-1.3C34.5,6.2,37.4,9.5,37.4,12.8z M43.3,12.8c0-6.2-4.2-12.8-14-12.8H10C9.6,0,9.3,0.2,9.3,0.6 c0,0.2,0.1,0.4,0.3,0.5c0.7,0.6,1.7,1.2,3.1,1.9c1.3,0.7,2.4,1.1,3.4,1.5c0.4,0.2,0.6,0.4,0.6,0.6c0,0.3-0.2,0.6-0.7,0.6H0.7 C0.2,5.6,0,5.9,0,6.2c0,0.3,0.1,0.5,0.3,0.7c1.2,1.3,3.2,2.9,6.1,4.7c2.5,1.6,5.7,3.3,8.2,4.5c0.4,0.2,0.6,0.4,0.6,0.7 c0,0.3-0.2,0.5-0.7,0.5H7.4c-0.4,0-0.6,0.2-0.6,0.5c0,0.2,0.1,0.4,0.3,0.6c1.6,1.5,4.2,3,7.6,4.5c4.6,1.9,9.2,3.1,14.4,3.1 C39,26,43.3,18.6,43.3,12.8z M30.4,21.7c-4.8,0-8.9-3.9-8.9-8.9c0-4.8,4-8.7,8.9-8.7c5,0,8.9,3.9,8.9,8.7 C39.3,17.7,35.4,21.7,30.4,21.7z'
/>
</svg>
)
}
export function AhrefsIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1065 1300'>
+6
View File
@@ -156,6 +156,7 @@ import {
MicrosoftSharepointIcon,
MicrosoftSqlIcon,
MicrosoftTeamsIcon,
MicrosoftWordIcon,
MillionVerifierIcon,
MintlifyIcon,
MistralIcon,
@@ -210,6 +211,7 @@ import {
SapS4HanaIcon,
SESIcon,
SecretsManagerIcon,
SemrushIcon,
SendblueIcon,
SendgridIcon,
SentryIcon,
@@ -447,6 +449,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
microsoft_excel_v2: MicrosoftExcelIcon,
microsoft_planner: MicrosoftPlannerIcon,
microsoft_teams: MicrosoftTeamsIcon,
microsoft_word: MicrosoftWordIcon,
'microsoft-teams': MicrosoftTeamsIcon,
millionverifier: MillionVerifierIcon,
mintlify: MintlifyIcon,
@@ -507,6 +510,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
sap_concur: SapConcurIcon,
sap_s4hana: SapS4HanaIcon,
secrets_manager: SecretsManagerIcon,
semrush: SemrushIcon,
sendblue: SendblueIcon,
sendgrid: SendgridIcon,
sentry: SentryIcon,
@@ -521,6 +525,8 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
similarweb: SimilarwebIcon,
sixtyfour: SixtyfourIcon,
slack: SlackIcon,
slack_app: SlackIcon,
slack_v2: SlackIcon,
smartlead: SmartleadIcon,
smtp: SmtpIcon,
snowflake: SnowflakeIcon,
@@ -22,6 +22,8 @@
"(generated)/credentials",
"(generated)/secrets",
"(generated)/billing",
"(generated)/catalog",
"(generated)/meta",
"(generated)/audit-logs"
]
}
@@ -1,3 +1,9 @@
{
"pages": ["listWorkflowRunsV2", "getWorkflowRunV2", "resumeWorkflowRunV2", "cancelRunV2"]
"pages": [
"listWorkflowRunsV2",
"getWorkflowRunV2",
"downloadWorkflowRunFileV2",
"resumeWorkflowRunV2",
"cancelRunV2"
]
}
@@ -5,14 +5,29 @@
"getWorkflow",
"updateWorkflowV2",
"deleteWorkflowV2",
"restoreWorkflow",
"duplicateWorkflow",
"moveWorkflows",
"getWorkflowState",
"replaceWorkflowState",
"applyWorkflowOperations",
"applyWorkflowVariables",
"listWorkflowVersionsV2",
"getWorkflowVersionV2",
"updateWorkflowVersionV2",
"activateWorkflowVersion",
"revertWorkflowVersion",
"exportWorkflow",
"importWorkflow",
"getWorkflowDeployment",
"updateWorkflowPublicApi",
"deployWorkflow",
"undeployWorkflow",
"rollbackWorkflow",
"listChatDeployments",
"getWorkflowChatDeployment",
"replaceWorkflowChatDeployment",
"deleteWorkflowChatDeployment",
"executeWorkflowV2",
"listWorkflowsFolders",
"createWorkflowsFolder",
@@ -22,6 +22,8 @@
"(generated)/credentials",
"(generated)/secrets",
"(generated)/billing",
"(generated)/catalog",
"(generated)/meta",
"(generated)/audit-logs"
]
}
+4 -4
View File
@@ -12,7 +12,7 @@ Every command below also accepts the [global options](/cli/commands#global-optio
## Get audit log
```bash
sim audit-logs get <id> [options]
sim audit-logs get <auditLogId> [options]
```
**Arguments**
@@ -21,7 +21,7 @@ sim audit-logs get <id> [options]
| Argument | Required | Description |
| --- | --- | --- |
| `id` | Yes | Audit-log entry identifier. |
| `auditLogId` | Yes | Audit-log entry identifier. |
</CommandTable>
@@ -31,7 +31,7 @@ sim audit-logs get <id> [options]
| Option | Required | Description |
| --- | --- | --- |
| `--organization <value>` | Yes | Organization ID (personal API key required). |
| `--organization <value>` | No | Organization ID; defaults to your only organization, and is required when your account belongs to more than one (personal API key required). |
</CommandTable>
@@ -55,7 +55,7 @@ sim audit-logs list [options]
| `--include-departed` | No | Include actions by users who have left the organization. |
| `--no-include-departed` | No | Send --include-departed as false. |
| `--limit <n>` | No | Maximum items to return (0 for everything). Defaults to `100`. |
| `--organization <value>` | Yes | Organization ID (personal API key required). |
| `--organization <value>` | No | Organization ID; defaults to your only organization, and is required when your account belongs to more than one (personal API key required). |
| `--actor-email <value>` | No | Filter by actor email address. |
| `--all-workspaces` | No | Do not filter to the configured workspace (personal API key required for account-wide access). |
@@ -13,6 +13,8 @@ Every command below also accepts the [global options](/cli/commands#global-optio
sim billing status [options]
```
Show billing status and current-period credit usage (credits and storage require a personal API key)
**Options**
<CommandTable>
+46
View File
@@ -0,0 +1,46 @@
---
title: Blocks
description: Manage blocks — every subcommand, argument, and flag
---
import { CommandTable } from '@/components/ui/command-table'
Every command below also accepts the [global options](/cli/commands#global-options).
## Get block
```bash
sim blocks get <blockId>
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `blockId` | Yes | Block type identifier. An unversioned base type resolves to the newest version, and the response echoes the resolved id. |
</CommandTable>
## List blocks
```bash
sim blocks list [options]
```
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--search <value>` | No | Case-insensitive substring match against the block id, name, and description. |
| `--category <value>` | No | Restrict to one toolbar category. Accepted values: `blocks`, `tools`, `triggers`. |
| `--capability <value>` | No | Restrict to blocks that can start a workflow — the `triggers` category, blocks declaring `triggerAllowed`, and blocks with trigger-mode fields. Accepted values: `trigger`. |
| `--source <value>` | No | Restrict to shipped blocks or to this workspaces deployed custom blocks. Accepted values: `builtin`, `custom`. |
| `--sort-by <value>` | No | Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order. Accepted values: `id`, `name`, `category`. |
| `--sort-order <value>` | No | Sort direction. Accepted values: `asc`, `desc`. |
| `--limit <n>` | No | Maximum items to return (0 for everything). Defaults to `100`. |
</CommandTable>
@@ -0,0 +1,29 @@
---
title: Chat Deployments
description: Manage chat deployments — every subcommand, argument, and flag
---
import { CommandTable } from '@/components/ui/command-table'
Every command below also accepts the [global options](/cli/commands#global-options).
## List chat deployments
```bash
sim chat-deployments list [options]
```
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--workflow-id <value>` | No | Restrict to deployments of one workflow. |
| `--is-active` | No | Restrict to active or inactive deployments. |
| `--no-is-active` | No | Send --is-active as false. |
| `--sort-by <value>` | No | Field used to sort the result. Accepted values: `identifier`, `createdAt`, `updatedAt`. |
| `--sort-order <value>` | No | Sort direction. Accepted values: `asc`, `desc`. |
| `--limit <n>` | No | Maximum items to return (0 for everything). Defaults to `100`. |
</CommandTable>
@@ -33,15 +33,21 @@ These apply to every command, and may be written before or after it.
| [`sim profiles`](/cli/profiles) | List profiles or add a workspace profile that shares a stored login |
| [`sim audit-logs`](/cli/audit-logs) | Manage audit logs |
| [`sim billing`](/cli/billing) | Manage billing |
| [`sim blocks`](/cli/blocks) | Manage blocks |
| [`sim chat-deployments`](/cli/chat-deployments) | Manage chat deployments |
| [`sim connector-types`](/cli/connector-types) | Manage connector types |
| [`sim credentials`](/cli/credentials) | Manage credentials |
| [`sim custom-tools`](/cli/custom-tools) | Manage custom tools |
| [`sim files`](/cli/files) | Manage files |
| [`sim knowledge`](/cli/knowledge) | Manage knowledge |
| [`sim logs`](/cli/logs) | Manage logs |
| [`sim mcp-servers`](/cli/mcp-servers) | Manage mcp servers |
| [`sim meta`](/cli/meta) | Manage meta |
| [`sim secrets`](/cli/secrets) | Manage secrets |
| [`sim skills`](/cli/skills) | Manage skills |
| [`sim tables`](/cli/tables) | Manage tables |
| [`sim tools`](/cli/tools) | Manage tools |
| [`sim workflow-mcp-servers`](/cli/workflow-mcp-servers) | Manage workflow mcp servers |
| [`sim workflows`](/cli/workflows) | Manage workflows |
| [`sim workspaces`](/cli/workspaces) | Manage workspaces |
@@ -0,0 +1,24 @@
---
title: Connector Types
description: Manage connector types — every subcommand, argument, and flag
---
import { CommandTable } from '@/components/ui/command-table'
Every command below also accepts the [global options](/cli/commands#global-options).
## List connector types
```bash
sim connector-types list [options]
```
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--search <value>` | No | Case-insensitive substring match against the connector name. |
</CommandTable>
@@ -72,6 +72,47 @@ sim credentials list [options]
</CommandTable>
## Update credential
```bash
sim credentials update <credentialId> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `credentialId` | Yes | Credential to update. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--display-name <value>` | No | New name shown for the credential in Sim. |
| `--description <value>` | No | New credential description. Send null to clear the stored one. (--description null sends the word, not JSON null). |
| `--service-account-json <value>` | No | Write-only Google service-account JSON key. |
| `--api-token <value>` | No | Write-only provider API token. |
| `--domain <value>` | No | Provider account domain. |
| `--signing-secret <value>` | No | Write-only webhook signing secret. |
| `--bot-token <value>` | No | Write-only bot token. |
| `--client-id <value>` | No | OAuth client identifier. |
| `--client-secret <value>` | No | Write-only OAuth client secret. |
| `--certificate-id <value>` | No | Provider certificate mapping identifier. |
| `--org-id <value>` | No | Provider organization ID. |
| `--data-center <value>` | No | Provider data center. |
| `--auth-method <value>` | No | Provider authentication method. |
| `--private-key <value>` | No | Write-only PEM private key. |
| `--username <value>` | No | Provider run-as username. |
| `--name <displayName>` | No | Alias for --display-name. |
</CommandTable>
## Create a service-account credential using its discovered provider schema
```bash
@@ -30,7 +30,7 @@ sim custom-tools create [options]
## Delete custom tool
```bash
sim custom-tools delete <id> [options]
sim custom-tools delete <customToolId> [options]
```
**Arguments**
@@ -39,7 +39,7 @@ sim custom-tools delete <id> [options]
| Argument | Required | Description |
| --- | --- | --- |
| `id` | Yes | Unique custom tool identifier. |
| `customToolId` | Yes | Unique custom tool identifier. |
</CommandTable>
@@ -56,7 +56,7 @@ sim custom-tools delete <id> [options]
## Get custom tool
```bash
sim custom-tools get <id>
sim custom-tools get <customToolId>
```
**Arguments**
@@ -65,7 +65,7 @@ sim custom-tools get <id>
| Argument | Required | Description |
| --- | --- | --- |
| `id` | Yes | Unique custom tool identifier. |
| `customToolId` | Yes | Unique custom tool identifier. |
</CommandTable>
@@ -91,7 +91,7 @@ sim custom-tools list [options]
## Update custom tool
```bash
sim custom-tools update <id> [options]
sim custom-tools update <customToolId> [options]
```
**Arguments**
@@ -100,7 +100,7 @@ sim custom-tools update <id> [options]
| Argument | Required | Description |
| --- | --- | --- |
| `id` | Yes | Unique custom tool identifier. |
| `customToolId` | Yes | Unique custom tool identifier. |
</CommandTable>
+71 -1
View File
@@ -107,6 +107,7 @@ Also available as `sim files folders ls`.
| `--search <value>` | No | Case-insensitive substring match against the folder name. |
| `--sort-by <value>` | No | Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order. Accepted values: `name`, `createdAt`, `updatedAt`. |
| `--sort-order <value>` | No | Sort direction. Accepted values: `asc`, `desc`. |
| `--scope <value>` | No | Which lifecycle set to list: `active` (default) returns live folders only; `archived` returns folders a recursive `DELETE` soft-deleted, which is how a caller finds a path to hand to `POST /api/v2/files/folders/restore`. Authorization is identical for both. Accepted values: `active`, `archived`. |
</CommandTable>
@@ -129,6 +130,22 @@ Also available as `sim files folders mv`.
</CommandTable>
## Restore an archived file folder
```bash
sim files folders restore <path>
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `path` | Yes | Folder path as shown in the app; the leading / is optional |
</CommandTable>
## Delete file
```bash
@@ -239,7 +256,8 @@ sim files list [options]
| Option | Required | Description |
| --- | --- | --- |
| `--folder <value>` | No | Folder path as shown in the app; the leading / is optional. |
| `--recursive <value>` | No | Whether the folder filter includes files in subfolders. Defaults to true when a search is set, false otherwise, so listing a folder shows that folder while searching one looks through everything in it. Ignored when no folder filter is set, which already spans the workspace. The listed spellings are the whole accepted vocabulary and are case-sensitive; any other value is rejected. Accepted values: `true`, `1`, `yes`, `on`, `y`, `enabled`, `false`, `0`, `no`, `off`, `n`, `disabled`. |
| `--recursive` | No | Whether the folder filter includes files in subfolders. Defaults to true when a search is set, false otherwise, so listing a folder shows that folder while searching one looks through everything in it. Ignored when no folder filter is set, which already spans the workspace. |
| `--no-recursive` | No | Send --recursive as false. |
| `--scope <value>` | No | Which lifecycle set to list: `active` (default) for live files, `archived` for files a `DELETE` soft-deleted. `folderPath` resolves against active folders only, so pairing it with `scope=archived` returns an empty page when the containing folder was archived too. Accepted values: `active`, `archived`. |
| `--search <value>` | No | Case-insensitive substring match against the file name. |
| `--sort-by <value>` | No | Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order. Accepted values: `name`, `size`, `uploadedAt`, `updatedAt`. |
@@ -267,6 +285,32 @@ Also available as `sim files mv`.
</CommandTable>
## Read a files text content
```bash
sim files read <fileId> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `fileId` | Yes | File identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--max-bytes <value>` | No | Optional ceiling on the source bytes fed to the parser, lowering but never raising the server limit. |
</CommandTable>
## Rename a file
```bash
@@ -309,6 +353,32 @@ sim files restore <fileId>
</CommandTable>
## Unzip an archive into a new folder beside it
```bash
sim files unzip <fileId> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `fileId` | Yes | File identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `-y, --yes` | Yes | Confirm this destructive operation. |
</CommandTable>
## Replace a files contents
```bash
+6
View File
@@ -139,6 +139,12 @@ sim tables rows query --help
| [`billing`](/cli/billing) | Check plan status and credit usage |
| [`audit-logs`](/cli/audit-logs) | Read organization audit logs |
| [`workspaces`](/cli/workspaces) | Inspect the active workspace and its members |
| [`blocks`](/cli/blocks) | Browse the block catalog and read one block's configuration fields |
| [`tools`](/cli/tools) | Browse the tool catalog |
| [`connector-types`](/cli/connector-types) | Browse knowledge-base connector types and their config fields |
| [`chat-deployments`](/cli/chat-deployments) | List the hosted chats a workspace serves |
| [`workflow-mcp-servers`](/cli/workflow-mcp-servers) | Publish workflows as MCP tools for outside agents |
| [`meta`](/cli/meta) | Check what this API supports and which limits apply |
The [command reference](/cli/commands) documents every subcommand, argument, and
flag, and is generated from the CLI itself.
+397 -11
View File
@@ -9,6 +9,392 @@ import { CommandTable } from '@/components/ui/command-table'
Every command below also accepts the [global options](/cli/commands#global-options).
## Index files the workspace already stores
```bash
sim knowledge from-workspace-files create <knowledgeBaseId> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `knowledgeBaseId` | Yes | Unique knowledge base identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--file <value...>` | Yes | Workspace file ID or key (repeatable) (space-separated, or @path / @- with one value per line). |
</CommandTable>
## Declare the tag definitions a knowledge base needs
```bash
sim knowledge tags save <knowledgeBaseId> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `knowledgeBaseId` | Yes | Unique knowledge base identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--definitions <json\|@file>` | Yes | Tag definitions: [&#123;"tagSlot":"tag1","displayName":"category","fieldType":"text"&#125;] (JSON, or @path / @- to read a file or stdin). |
</CommandTable>
## Create tag
```bash
sim knowledge tags create <knowledgeBaseId> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `knowledgeBaseId` | Yes | Unique knowledge base identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--display-name <value>` | Yes | Name tag filters and document reads use for this tag. |
| `--field-type <value>` | No | Value type stored in the slot; it decides which slots are usable and which filter operators apply. Defaults to text, so a number, date, or boolean slot must name its type here. Slot capacity per type: text 7, number 5, date 2, boolean 3. Accepted values: `text`, `number`, `date`, `boolean`. |
| `--tag-slot <value>` | No | Slot to store the tag in. Omit to take the next free slot for the field type; a slot that does not belong to the field type, or one already in use, is rejected. Accepted values: `tag1`, `tag2`, `tag3`, `tag4`, `tag5`, `tag6`, `tag7`, `number1`, `number2`, `number3`, `number4`, `number5`, `date1`, `date2`, `boolean1`, `boolean2`, `boolean3`. |
</CommandTable>
## Delete tag
```bash
sim knowledge tags delete <knowledgeBaseId> <tagId> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `knowledgeBaseId` | Yes | Unique knowledge base identifier. |
| `tagId` | Yes | Unique tag definition identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `-y, --yes` | Yes | Confirm this destructive operation. |
</CommandTable>
## Remove tag definitions no document still uses
```bash
sim knowledge tags cleanup <knowledgeBaseId> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `knowledgeBaseId` | Yes | Unique knowledge base identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--unused` | No | Whether to remove only the tag definitions no document in the knowledge base still carries a value for. Defaults to true. Pass --no-unused to delete every definition on the knowledge base, which also clears its slot on every document and chunk and is not recoverable. |
| `--no-unused` | No | Send --unused as false. |
| `-y, --yes` | Yes | Confirm this destructive operation. |
</CommandTable>
## Show which tag slot a create would take for a field type
```bash
sim knowledge tags next-slot <knowledgeBaseId> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `knowledgeBaseId` | Yes | Unique knowledge base identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--field-type <value>` | Yes | Value type stored in the slot; it decides which slots are usable and which filter operators apply. Slot capacity per type: text 7, number 5, date 2, boolean 3. Accepted values: `text`, `number`, `date`, `boolean`. |
</CommandTable>
## List tags
```bash
sim knowledge tags list <knowledgeBaseId>
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `knowledgeBaseId` | Yes | Unique knowledge base identifier. |
</CommandTable>
## Show how many documents and chunks carry each tag
```bash
sim knowledge tags usage <knowledgeBaseId>
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `knowledgeBaseId` | Yes | Unique knowledge base identifier. |
</CommandTable>
## Update tag
```bash
sim knowledge tags update <knowledgeBaseId> <tagId> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `knowledgeBaseId` | Yes | Unique knowledge base identifier. |
| `tagId` | Yes | Unique tag definition identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--display-name <value>` | No | New tag display name. |
| `--field-type <value>` | No | New value type for the tag. Accepted values: `text`, `number`, `date`, `boolean`. |
</CommandTable>
## Enable, disable, or delete many chunks at once
```bash
sim knowledge chunks batch-update <knowledgeBaseId> <documentId> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `knowledgeBaseId` | Yes | Unique knowledge base identifier. |
| `documentId` | Yes | Unique knowledge document identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--operation <value>` | Yes | What to do with the selected chunks. Accepted values: `enable`, `disable`, `delete`. |
| `--chunk <value...>` | Yes | Chunks to operate on, by identifier. Ids outside the document are ignored. (space-separated, or @path / @- with one value per line). |
| `-y, --yes` | Yes | Confirm this destructive operation. |
</CommandTable>
## Create chunk
```bash
sim knowledge chunks create <knowledgeBaseId> <documentId> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `knowledgeBaseId` | Yes | Unique knowledge base identifier. |
| `documentId` | Yes | Unique knowledge document identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--content <value>` | Yes | Text to embed. It is embedded on write, so the chunk is searchable immediately. |
| `--enabled` | No | Whether the new chunk participates in search. |
| `--no-enabled` | No | Send --enabled as false. |
</CommandTable>
## Delete chunk
```bash
sim knowledge chunks delete <knowledgeBaseId> <documentId> <chunkId> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `knowledgeBaseId` | Yes | Unique knowledge base identifier. |
| `documentId` | Yes | Unique knowledge document identifier. |
| `chunkId` | Yes | Unique chunk identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `-y, --yes` | Yes | Confirm this destructive operation. |
</CommandTable>
## Get chunk
```bash
sim knowledge chunks get <knowledgeBaseId> <documentId> <chunkId>
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `knowledgeBaseId` | Yes | Unique knowledge base identifier. |
| `documentId` | Yes | Unique knowledge document identifier. |
| `chunkId` | Yes | Unique chunk identifier. |
</CommandTable>
## List chunks
```bash
sim knowledge chunks list <knowledgeBaseId> <documentId> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `knowledgeBaseId` | Yes | Unique knowledge base identifier. |
| `documentId` | Yes | Unique knowledge document identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--search <value>` | No | Case-insensitive substring match against chunk content. |
| `--enabled <value>` | No | Restrict to enabled or disabled chunks. `all` returns both. Accepted values: `true`, `false`, `all`. |
| `--sort-by <value>` | No | Field used to sort the result. Accepted values: `chunkIndex`, `tokenCount`, `enabled`. |
| `--sort-order <value>` | No | Sort direction. Accepted values: `asc`, `desc`. |
| `--limit <n>` | No | Maximum items to return (0 for everything). Defaults to `100`. |
</CommandTable>
## Update chunk
```bash
sim knowledge chunks update <knowledgeBaseId> <documentId> <chunkId> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `knowledgeBaseId` | Yes | Unique knowledge base identifier. |
| `documentId` | Yes | Unique knowledge document identifier. |
| `chunkId` | Yes | Unique chunk identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--content <value>` | No | Replacement text. Changing it re-embeds the chunk and re-derives its token and character counts. |
| `--enabled` | No | Whether the chunk participates in search. Disabling keeps it indexed. |
| `--no-enabled` | No | Send --enabled as false. |
</CommandTable>
## Enable or disable every matching document
```bash
@@ -160,7 +546,6 @@ sim knowledge documents update <knowledgeBaseId> <documentId> [options]
| `--boolean3` | No | New value for boolean tag slot 3. |
| `--no-boolean3` | No | Send --boolean3 as false. |
| `--retry-processing` | No | Requeue a failed or stuck document for processing. Send it alone — no other field may accompany it — and it answers with a queue acknowledgement rather than the document. |
| `--no-retry-processing` | No | Send --retry-processing as false. |
</CommandTable>
@@ -517,7 +902,7 @@ Also available as `sim knowledge folders mv`.
## Delete knowledge base
```bash
sim knowledge delete <id> [options]
sim knowledge delete <knowledgeBaseId> [options]
```
**Arguments**
@@ -526,7 +911,7 @@ sim knowledge delete <id> [options]
| Argument | Required | Description |
| --- | --- | --- |
| `id` | Yes | Unique knowledge base identifier. |
| `knowledgeBaseId` | Yes | Unique knowledge base identifier. |
</CommandTable>
@@ -543,7 +928,7 @@ sim knowledge delete <id> [options]
## Get knowledge base
```bash
sim knowledge get <id>
sim knowledge get <knowledgeBaseId>
```
**Arguments**
@@ -552,7 +937,7 @@ sim knowledge get <id>
| Argument | Required | Description |
| --- | --- | --- |
| `id` | Yes | Unique knowledge base identifier. |
| `knowledgeBaseId` | Yes | Unique knowledge base identifier. |
</CommandTable>
@@ -568,6 +953,7 @@ sim knowledge list [options]
| Option | Required | Description |
| --- | --- | --- |
| `--scope <value>` | No | Which lifecycle set to list: `active` (default) for live knowledge bases, `archived` for knowledge bases a `DELETE` archived and `POST /knowledge/&#123;knowledgeBaseId&#125;/restore` can bring back. `folderPath` resolves against active folders only, so pairing it with `scope=archived` returns an empty page when the containing folder was archived too. Accepted values: `active`, `archived`. |
| `--folder <value>` | No | Folder path as shown in the app; the leading / is optional. |
| `--search <value>` | No | Case-insensitive substring match against the resource name. |
| `--sort-by <value>` | No | Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order. Accepted values: `name`, `createdAt`, `updatedAt`. |
@@ -576,10 +962,10 @@ sim knowledge list [options]
</CommandTable>
## List tags
## Restore an archived knowledge base
```bash
sim knowledge tags list <knowledgeBaseId>
sim knowledge restore <knowledgeBaseId>
```
**Arguments**
@@ -619,7 +1005,7 @@ sim knowledge search [options]
## Update knowledge base
```bash
sim knowledge update <id> [options]
sim knowledge update <knowledgeBaseId> [options]
```
**Arguments**
@@ -628,7 +1014,7 @@ sim knowledge update <id> [options]
| Argument | Required | Description |
| --- | --- | --- |
| `id` | Yes | Unique knowledge base identifier. |
| `knowledgeBaseId` | Yes | Unique knowledge base identifier. |
</CommandTable>
@@ -648,7 +1034,7 @@ sim knowledge update <id> [options]
## Move a knowledge base to a folder
```bash
sim knowledge mv <id> <folder>
sim knowledge mv <knowledgeBaseId> <folder>
```
**Arguments**
@@ -657,7 +1043,7 @@ sim knowledge mv <id> <folder>
| Argument | Required | Description |
| --- | --- | --- |
| `id` | Yes | Unique knowledge base identifier. |
| `knowledgeBaseId` | Yes | Unique knowledge base identifier. |
| `folder` | Yes | Folder path as shown in the app; the leading / is optional |
</CommandTable>
+30 -3
View File
@@ -35,6 +35,28 @@ sim logs get <runId> [options]
</CommandTable>
## Summarize run counts, failures, and cost over a window
```bash
sim logs stats [options]
```
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--workflow <value...>` | No | Comma-separated workflow identifiers to include. At most 200 entries. An empty entry is rejected. (space-separated, or @path / @- with one value per line). |
| `--folder <value...>` | No | Folder path as shown in the app; the leading / is optional (space-separated, or @path / @- with one value per line). |
| `--trigger <value...>` | No | Comma-separated trigger types to include. An empty entry is rejected. The vocabulary is open, so an unrecognized member selects no runs; the literal `all` disables this filter. (space-separated, or @path / @- with one value per line). |
| `--level <value>` | No | Severity level to include. Accepted values: `info`, `error`. |
| `--start-date <value>` | No | Only include runs started at or after this UTC ISO 8601 timestamp, e.g. `2026-08-06T00:00:00Z`. A date without a time, or a timestamp carrying a UTC offset instead of `Z`, is rejected, as is year `0000`, which names no storable instant. |
| `--end-date <value>` | No | Only include runs started at or before this UTC ISO 8601 timestamp, e.g. `2026-08-06T00:00:00Z`. A date without a time, or a timestamp carrying a UTC offset instead of `Z`, is rejected, as is year `0000`, which names no storable instant. |
| `--segment-count <value>` | No | Number of equal time buckets to divide the window into, from 1 to 500. Exactly this many buckets are always returned. Buckets are never narrower than one minute, so on a short window the series extends past the end of the window rather than being compressed, and the trailing buckets are empty. |
</CommandTable>
## List logs
```bash
@@ -47,8 +69,8 @@ sim logs list [options]
| Option | Required | Description |
| --- | --- | --- |
| `--workflow <value...>` | No | Comma-separated workflow identifiers to include. An empty entry is rejected. (space-separated, or @path / @- with one value per line). |
| `--trigger <value...>` | No | Comma-separated trigger types to include. An empty entry is rejected. Values are matched exactly and are case-sensitive — every recorded trigger is lowercase, so `API` matches nothing while `api` matches. The vocabulary is open: it covers the core trigger types (`manual`, `api`, `schedule`, `chat`, `webhook`, `mcp`, `copilot`, `workflow`, `custom_block`) and the provider id of any webhook trigger (`slack`, `gmail`, `github`, …), so an unrecognized member is not rejected — it selects no runs. The literal value `all` is a sentinel that disables this filter entirely, so a list containing it returns runs of every trigger type; no real trigger type is named `all`. (space-separated, or @path / @- with one value per line). |
| `--workflow <value...>` | No | Comma-separated workflow identifiers to include. An empty entry is rejected. At most 200 entries. (space-separated, or @path / @- with one value per line). |
| `--trigger <value...>` | No | Comma-separated trigger types to include. An empty entry is rejected. Values are matched exactly and are case-sensitive — every recorded trigger is lowercase, so `API` matches nothing while `api` matches. The vocabulary is open: it covers the core trigger types (`manual`, `api`, `schedule`, `chat`, `webhook`, `mcp`, `copilot`, `workflow`, `custom_block`) and the provider id of any webhook trigger (`slack`, `gmail`, `github`, …), so an unrecognized member is not rejected — it selects no runs. The literal value `all` is a sentinel that disables this filter entirely, so a list containing it returns runs of every trigger type; no real trigger type is named `all`. At most 100 entries. (space-separated, or @path / @- with one value per line). |
| `--level <value>` | No | Severity level to include. Accepted values: `info`, `error`. |
| `--start-date <value>` | No | Only include runs started at or after this UTC ISO 8601 timestamp, e.g. `2026-08-06T00:00:00Z`. A date without a time, or a timestamp carrying a UTC offset instead of `Z`, is rejected, as is year `0000`, which names no storable instant. |
| `--end-date <value>` | No | Only include runs started at or before this UTC ISO 8601 timestamp, e.g. `2026-08-06T00:00:00Z`. A date without a time, or a timestamp carrying a UTC offset instead of `Z`, is rejected, as is year `0000`, which names no storable instant. |
@@ -61,8 +83,13 @@ sim logs list [options]
| `--include-trace-spans` | No | Include trace spans in JSON or YAML output (implies full detail). |
| `--include-final-output` | No | Include final output in JSON or YAML output (implies full detail). |
| `--limit <n>` | No | Maximum items to return (0 for everything). Defaults to `100`. |
| `--order <value>` | No | Sort direction by execution start time. This list is sortable only by execution start time, so it takes `order` in place of `sortBy`/`sortOrder`, which it rejects. Accepted values: `asc`, `desc`. |
| `--status <value>` | No | Comma-separated execution statuses to include, from `pending` \| `running` \| `paused` \| `redacting` \| `completed` \| `failed` \| `cancelled`. An empty entry is rejected. ANDed with `level`, which reports severity rather than lifecycle. |
| `--workflow-name <value>` | No | Case-insensitive substring match against the run's workflow name. Runs whose workflow has been deleted match nothing, because the name is no longer joinable. |
| `--include-job-runs` | No | Whether Chat and Sim-agent job runs join the sequence alongside workflow runs. Job runs report `kind: "job"`, carry no `workflow` summary, and never carry a cost ledger. They are dropped entirely — not partially matched — whenever a filter they cannot answer is set (`workflowIds`, `workflowName`, `folderPaths`, `model`, or `status`), so a filter never means two different things across the union. Accepted only under `sortBy=startedAt`: job runs record cost as a document and no comparable status, so they cannot participate in the other orderings. |
| `--no-include-job-runs` | No | Send --include-job-runs as false. |
| `--run-id <value>` | No | Exact run identifier to match. |
| `--sort-by <value>` | No | Field used to sort the result. `durationMs` and `cost` are null until a run settles; those runs order as though the value were below every recorded one, so they trail an ascending page and lead a descending one. Only `startedAt` can order Chat and Sim-agent job runs, so any other value is rejected together with `includeJobRuns=true`. Accepted values: `startedAt`, `durationMs`, `cost`, `status`. |
| `--sort-order <value>` | No | Sort direction. Accepted values: `asc`, `desc`. |
| `--folder <value...>` | No | Folder path as shown in the app; the leading / is optional (space-separated, or @path / @- with one value per line). |
</CommandTable>
+10 -10
View File
@@ -32,14 +32,14 @@ sim mcp-servers create [options]
| `--enabled` | No | Whether the server tools are available to workflows. Applied server-side as true when omitted on create. |
| `--no-enabled` | No | Send --enabled as false. |
| `--oauth-client-id <value>` | No | Pre-registered OAuth client identifier. Changing it on update revokes the stored OAuth grant and forces reauthorization. |
| `--oauth-client-secret <value>` | No | Write-only pre-registered OAuth client secret. Sending it on update as null or a new value revokes the stored OAuth grant and forces reauthorization, as does switching away from OAuth authentication. |
| `--oauth-client-secret <value>` | No | Write-only pre-registered OAuth client secret. Sending it on update as null or a new value revokes the stored OAuth grant and forces reauthorization, as does switching away from OAuth authentication. (--oauth-client-secret null sends the word, not JSON null). |
</CommandTable>
## Delete MCP server
```bash
sim mcp-servers delete <id> [options]
sim mcp-servers delete <mcpServerId> [options]
```
**Arguments**
@@ -48,7 +48,7 @@ sim mcp-servers delete <id> [options]
| Argument | Required | Description |
| --- | --- | --- |
| `id` | Yes | Unique MCP server identifier. |
| `mcpServerId` | Yes | Unique MCP server identifier. |
</CommandTable>
@@ -65,7 +65,7 @@ sim mcp-servers delete <id> [options]
## Get MCP server
```bash
sim mcp-servers get <id>
sim mcp-servers get <mcpServerId>
```
**Arguments**
@@ -74,7 +74,7 @@ sim mcp-servers get <id>
| Argument | Required | Description |
| --- | --- | --- |
| `id` | Yes | Unique MCP server identifier. |
| `mcpServerId` | Yes | Unique MCP server identifier. |
</CommandTable>
@@ -100,7 +100,7 @@ sim mcp-servers list [options]
## List MCP server tools
```bash
sim mcp-servers tools list <id> [options]
sim mcp-servers tools list <mcpServerId> [options]
```
**Arguments**
@@ -109,7 +109,7 @@ sim mcp-servers tools list <id> [options]
| Argument | Required | Description |
| --- | --- | --- |
| `id` | Yes | Unique MCP server identifier. |
| `mcpServerId` | Yes | Unique MCP server identifier. |
</CommandTable>
@@ -127,7 +127,7 @@ sim mcp-servers tools list <id> [options]
## Update MCP server
```bash
sim mcp-servers update <id> [options]
sim mcp-servers update <mcpServerId> [options]
```
**Arguments**
@@ -136,7 +136,7 @@ sim mcp-servers update <id> [options]
| Argument | Required | Description |
| --- | --- | --- |
| `id` | Yes | Unique MCP server identifier. |
| `mcpServerId` | Yes | Unique MCP server identifier. |
</CommandTable>
@@ -157,6 +157,6 @@ sim mcp-servers update <id> [options]
| `--enabled` | No | Whether the server tools are available to workflows. Applied server-side as true when omitted on create. |
| `--no-enabled` | No | Send --enabled as false. |
| `--oauth-client-id <value>` | No | Pre-registered OAuth client identifier. Changing it on update revokes the stored OAuth grant and forces reauthorization. |
| `--oauth-client-secret <value>` | No | Write-only pre-registered OAuth client secret. Sending it on update as null or a new value revokes the stored OAuth grant and forces reauthorization, as does switching away from OAuth authentication. |
| `--oauth-client-secret <value>` | No | Write-only pre-registered OAuth client secret. Sending it on update as null or a new value revokes the stored OAuth grant and forces reauthorization, as does switching away from OAuth authentication. (--oauth-client-secret null sends the word, not JSON null). |
</CommandTable>
+6
View File
@@ -14,15 +14,21 @@
"profiles",
"audit-logs",
"billing",
"blocks",
"chat-deployments",
"connector-types",
"credentials",
"custom-tools",
"files",
"knowledge",
"logs",
"mcp-servers",
"meta",
"secrets",
"skills",
"tables",
"tools",
"workflow-mcp-servers",
"workflows",
"workspaces",
"reference"
+14
View File
@@ -0,0 +1,14 @@
---
title: Meta
description: Manage meta — every subcommand, argument, and flag
---
import { CommandTable } from '@/components/ui/command-table'
Every command below also accepts the [global options](/cli/commands#global-options).
## Show what this API supports and which limits apply
```bash
sim meta status
```
File diff suppressed because it is too large Load Diff
+3 -1
View File
@@ -79,7 +79,9 @@ sim secrets set <name> [options]
| Option | Required | Description |
| --- | --- | --- |
| `--scope <scope>` | Yes | Secret ownership scope. Accepted values: `workspace`, `personal`. |
| `--value <value>` | No | Secret value; visible to shell history when supplied directly. |
| `--value <value\|@file>` | No | Secret value. Passing it inline exposes it to shell history and process listings; @path reads it from a file and @- from stdin, verbatim — a trailing newline is part of the value, so write the file with printf rather than echo. Prefix a literal leading @ with a second one. |
| `--description <description>` | No | What the secret is for, shown to teammates; workspace scope only. Omit to leave an existing description unchanged. |
| `--unredacted` | No | Opt the workspace secret out of redaction: its value then appears in plaintext in run logs, model-visible content, and files, including publicly shared log links. Workspace scope only — sending it for a personal secret is rejected. Omit it to leave the current setting untouched. Pass --no-unredacted to restore redaction. |
| `--no-unredacted` | No | Send --unredacted as false. |
</CommandTable>
+12 -12
View File
@@ -30,7 +30,7 @@ sim skills create [options]
## Delete skill
```bash
sim skills delete <id> [options]
sim skills delete <skillId> [options]
```
**Arguments**
@@ -39,7 +39,7 @@ sim skills delete <id> [options]
| Argument | Required | Description |
| --- | --- | --- |
| `id` | Yes | Unique skill identifier. A built-in skill is `builtin-` followed by its name, for example `builtin-research`. |
| `skillId` | Yes | Unique skill identifier. A built-in skill is `builtin-` followed by its name, for example `builtin-research`. |
</CommandTable>
@@ -56,7 +56,7 @@ sim skills delete <id> [options]
## Get skill
```bash
sim skills get <id>
sim skills get <skillId>
```
**Arguments**
@@ -65,14 +65,14 @@ sim skills get <id>
| Argument | Required | Description |
| --- | --- | --- |
| `id` | Yes | Unique skill identifier. A built-in skill is `builtin-` followed by its name, for example `builtin-research`. |
| `skillId` | Yes | Unique skill identifier. A built-in skill is `builtin-` followed by its name, for example `builtin-research`. |
</CommandTable>
## Grant skill editor
```bash
sim skills editors create <id> [options]
sim skills editors create <skillId> [options]
```
**Arguments**
@@ -81,7 +81,7 @@ sim skills editors create <id> [options]
| Argument | Required | Description |
| --- | --- | --- |
| `id` | Yes | Unique skill identifier. A built-in skill is `builtin-` followed by its name, for example `builtin-research`. |
| `skillId` | Yes | Unique skill identifier. A built-in skill is `builtin-` followed by its name, for example `builtin-research`. |
</CommandTable>
@@ -98,7 +98,7 @@ sim skills editors create <id> [options]
## List skill editors
```bash
sim skills editors list <id> [options]
sim skills editors list <skillId> [options]
```
**Arguments**
@@ -107,7 +107,7 @@ sim skills editors list <id> [options]
| Argument | Required | Description |
| --- | --- | --- |
| `id` | Yes | Unique skill identifier. A built-in skill is `builtin-` followed by its name, for example `builtin-research`. |
| `skillId` | Yes | Unique skill identifier. A built-in skill is `builtin-` followed by its name, for example `builtin-research`. |
</CommandTable>
@@ -126,7 +126,7 @@ sim skills editors list <id> [options]
## Revoke skill editor
```bash
sim skills editors delete <id> [options]
sim skills editors delete <skillId> [options]
```
**Arguments**
@@ -135,7 +135,7 @@ sim skills editors delete <id> [options]
| Argument | Required | Description |
| --- | --- | --- |
| `id` | Yes | Unique skill identifier. A built-in skill is `builtin-` followed by its name, for example `builtin-research`. |
| `skillId` | Yes | Unique skill identifier. A built-in skill is `builtin-` followed by its name, for example `builtin-research`. |
</CommandTable>
@@ -172,7 +172,7 @@ sim skills list [options]
## Update skill
```bash
sim skills update <id> [options]
sim skills update <skillId> [options]
```
**Arguments**
@@ -181,7 +181,7 @@ sim skills update <id> [options]
| Argument | Required | Description |
| --- | --- | --- |
| `id` | Yes | Unique skill identifier. A built-in skill is `builtin-` followed by its name, for example `builtin-research`. |
| `skillId` | Yes | Unique skill identifier. A built-in skill is `builtin-` followed by its name, for example `builtin-research`. |
</CommandTable>
+497 -305
View File
@@ -62,37 +62,6 @@ sim tables columns delete <tableId> [options]
</CommandTable>
## Run a columns workflow
```bash
sim tables columns run <tableId> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `tableId` | Yes | Unique table identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--group-ids <value...>` | Yes | Workflow or enrichment groups to run. (space-separated, or @path / @- with one value per line). |
| `--run-mode <value>` | No | Whether to run all or only incomplete cells. Accepted values: `all`, `incomplete`. |
| `--row-ids <value...>` | No | Explicit row subset to run. (space-separated, or @path / @- with one value per line). |
| `--filter <json\|@file>` | No | Predicate: &#123;"all":[&#123;"field":"status","op":"eq","value":"active"&#125;]&#125;; groups use all/any. Operators: eq, ne, gt, gte, lt, lte, in, nin, contains, ncontains, startsWith, endsWith, like, ilike, nlike, nilike, isEmpty, isNotEmpty, isNull, isNotNull (JSON, or @path / @- to read a file or stdin). |
| `--exclude-row-ids <value...>` | No | Rows excluded from a select-all run scope. (space-separated, or @path / @- with one value per line). |
| `--limit <json\|@file>` | No | Optional cap on eligible rows to run. (JSON, or @path / @- to read a file or stdin). |
</CommandTable>
## Update column
```bash
@@ -229,10 +198,28 @@ sim tables groups update <tableId> [options]
</CommandTable>
## Cancel table export
## Bulk delete tables and folders
```bash
sim tables exports cancel <exportId>
sim tables batch-delete [options]
```
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--table-ids <json\|@file>` | No | Tables to archive, by identifier. (JSON, or @path / @- to read a file or stdin). |
| `--folder <value...>` | No | Folder path as shown in the app; the leading / is optional (space-separated, or @path / @- with one value per line). |
| `-y, --yes` | Yes | Confirm this destructive operation. |
</CommandTable>
## Apply a distinct patch to each listed row
```bash
sim tables rows update-each <tableId> [options]
```
**Arguments**
@@ -241,6 +228,421 @@ sim tables exports cancel <exportId>
| Argument | Required | Description |
| --- | --- | --- |
| `tableId` | Yes | Unique table identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--updates <json\|@file>` | Yes | One merge patch per row. Each row identifier may appear at most once. (JSON, or @path / @- to read a file or stdin). |
</CommandTable>
## Create rows
```bash
sim tables rows create <tableId> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `tableId` | Yes | Unique table identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--data <json\|@file>` | No | One row keyed by column name (JSON, or @path / @-; choose exactly one body flag). |
| `--rows <json\|@file>` | No | Several rows keyed by column name (JSON, or @path / @-; choose exactly one body flag). |
</CommandTable>
## Delete row
```bash
sim tables rows delete <tableId> <rowId> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `tableId` | Yes | Unique table identifier. |
| `rowId` | Yes | Unique table row identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `-y, --yes` | Yes | Confirm this destructive operation. |
</CommandTable>
## Delete rows matching a filter, or an explicit list of ids
```bash
sim tables rows batch-delete <tableId> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `tableId` | Yes | Unique table identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--filter <json\|@file>` | No | Predicate: &#123;"all":[&#123;"field":"status","op":"eq","value":"active"&#125;]&#125;; groups use all/any. Operators: eq, ne, gt, gte, lt, lte, in, nin, contains, ncontains, startsWith, endsWith, like, ilike, nlike, nilike, isEmpty, isNotEmpty, isNull, isNotNull (JSON, or @path / @- to read a file or stdin). |
| `--limit <n>` | No | Maximum items to return (0 for everything). Defaults to `100`. |
| `--row <value...>` | No | Explicit row identifiers to delete. (space-separated, or @path / @- with one value per line). |
| `-y, --yes` | Yes | Confirm this destructive operation. |
</CommandTable>
## Get row
```bash
sim tables rows get <tableId> <rowId> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `tableId` | Yes | Unique table identifier. |
| `rowId` | Yes | Unique table row identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--include-run-state` | No | Include per-workflow-group run state on the returned row. Off by default. |
| `--no-include-run-state` | No | Send --include-run-state as false. |
</CommandTable>
## List rows
```bash
sim tables rows list <tableId> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `tableId` | Yes | Unique table identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--limit <n>` | No | Maximum items to return (0 for everything). Defaults to `100`. |
| `--include-run-state` | No | Include per-workflow-group run state on every returned row. Off by default: run state is a separate sidecar read and its `blockErrors` are unbounded, so a full page carries it only when asked. Caps `limit` at 200. |
| `--no-include-run-state` | No | Send --include-run-state as false. |
</CommandTable>
## Query rows
```bash
sim tables rows query <tableId> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `tableId` | Yes | Unique table identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--filter <json\|@file>` | No | Condition: &#123;"field":"status","op":"eq","value":"active"&#125;. Groups: &#123;"all":[&#123;"field":"status","op":"eq","value":"active"&#125;]&#125; or &#123;"any":[&#123;"field":"status","op":"eq","value":"active"&#125;]&#125;; group entries may also be nested groups. Operators: eq, ne, gt, gte, lt, lte, in, nin, contains, ncontains, startsWith, endsWith, like, ilike, nlike, nilike, isEmpty, isNotEmpty, isNull, isNotNull (JSON, or @path / @- to read a file or stdin). |
| `--sort <json\|@file>` | No | Ordered sort keys: [&#123;"field":"createdAt","direction":"desc"&#125;] (direction: asc or desc) (JSON, or @path / @- to read a file or stdin). |
| `--limit <n>` | No | Maximum items to return (0 for everything). Defaults to `100`. |
| `--include-run-state` | No | Include per-workflow-group run state on every returned row. Off by default: run state is a separate sidecar read and its `blockErrors` are unbounded, so a full page carries it only when asked. Incompatible with `limit: 0`, and caps `limit` at 200. |
| `--no-include-run-state` | No | Send --include-run-state as false. |
</CommandTable>
## Count rows matching a filter
```bash
sim tables rows count <tableId> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `tableId` | Yes | Unique table identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--filter <json\|@file>` | No | Condition: &#123;"field":"status","op":"eq","value":"active"&#125;. Groups: &#123;"all":[&#123;"field":"status","op":"eq","value":"active"&#125;]&#125; or &#123;"any":[&#123;"field":"status","op":"eq","value":"active"&#125;]&#125;; group entries may also be nested groups. Operators: eq, ne, gt, gte, lt, lte, in, nin, contains, ncontains, startsWith, endsWith, like, ilike, nlike, nilike, isEmpty, isNotEmpty, isNull, isNotNull (JSON, or @path / @- to read a file or stdin). |
</CommandTable>
## Run one rows enrichment group
```bash
sim tables rows enrich <tableId> <rowId> <groupId>
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `tableId` | Yes | Unique table identifier. |
| `rowId` | Yes | Unique table row identifier. |
| `groupId` | Yes | Workflow or enrichment group to run. |
</CommandTable>
## Search cells for a value and return their coordinates
```bash
sim tables rows search <tableId> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `tableId` | Yes | Unique table identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--query <value>` | Yes | Value to search for. |
| `--filter <json\|@file>` | No | Predicate: &#123;"all":[&#123;"field":"status","op":"eq","value":"active"&#125;]&#125;; groups use all/any. Operators: eq, ne, gt, gte, lt, lte, in, nin, contains, ncontains, startsWith, endsWith, like, ilike, nlike, nilike, isEmpty, isNotEmpty, isNull, isNotNull (JSON, or @path / @- to read a file or stdin). |
| `--sort <json\|@file>` | No | Ordered sort keys: [&#123;"field":"createdAt","direction":"desc"&#125;] (direction: asc or desc) (JSON, or @path / @- to read a file or stdin). |
</CommandTable>
## Update every row matching a filter
```bash
sim tables rows batch-update <tableId> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `tableId` | Yes | Unique table identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--filter <json\|@file>` | Yes | Predicate: &#123;"all":[&#123;"field":"status","op":"eq","value":"active"&#125;]&#125;; groups use all/any. Operators: eq, ne, gt, gte, lt, lte, in, nin, contains, ncontains, startsWith, endsWith, like, ilike, nlike, nilike, isEmpty, isNotEmpty, isNull, isNotNull (JSON, or @path / @- to read a file or stdin). |
| `--data <json\|@file>` | Yes | Row-data patch applied to every matching row. (JSON, or @path / @- to read a file or stdin). |
| `--limit <n>` | No | Maximum items to return (0 for everything). Defaults to `100`. |
| `-y, --yes` | Yes | Confirm this destructive operation. |
</CommandTable>
## Update row
```bash
sim tables rows update <tableId> <rowId> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `tableId` | Yes | Unique table identifier. |
| `rowId` | Yes | Unique table row identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--data <json\|@file>` | Yes | Partial row-data patch keyed by column name. (JSON, or @path / @- to read a file or stdin). |
</CommandTable>
## Cancel a running dispatch
```bash
sim tables dispatches cancel <tableId> <dispatchId> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `tableId` | Yes | Unique table identifier. |
| `dispatchId` | Yes | Unique table run-dispatch identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `-y, --yes` | Yes | Confirm this destructive operation. |
</CommandTable>
## Start a column or enrichment run
```bash
sim tables dispatches create <tableId> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `tableId` | Yes | Unique table identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--group-ids <value...>` | Yes | Workflow or enrichment groups to run. (space-separated, or @path / @- with one value per line). |
| `--run-mode <value>` | No | Whether to run all or only incomplete cells. Accepted values: `all`, `incomplete`. |
| `--row-ids <value...>` | No | Explicit row subset to run. (space-separated, or @path / @- with one value per line). |
| `--filter <json\|@file>` | No | Predicate: &#123;"all":[&#123;"field":"status","op":"eq","value":"active"&#125;]&#125;; groups use all/any. Operators: eq, ne, gt, gte, lt, lte, in, nin, contains, ncontains, startsWith, endsWith, like, ilike, nlike, nilike, isEmpty, isNotEmpty, isNull, isNotNull (JSON, or @path / @- to read a file or stdin). |
| `--exclude-row-ids <value...>` | No | Rows excluded from a select-all run scope. (space-separated, or @path / @- with one value per line). |
| `--max-rows <n>` | No | Stop after this many eligible rows have run (1-1,000,000). Omit for an unbounded run. |
</CommandTable>
## Get run dispatch
```bash
sim tables dispatches get <tableId> <dispatchId>
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `tableId` | Yes | Unique table identifier. |
| `dispatchId` | Yes | Unique table run-dispatch identifier. |
</CommandTable>
## List active run dispatches
```bash
sim tables dispatches list <tableId>
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `tableId` | Yes | Unique table identifier. |
</CommandTable>
## Cancel table export
```bash
sim tables exports cancel <tableId> <exportId>
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `tableId` | Yes | Unique table identifier. |
| `exportId` | Yes | Unique table-export identifier. |
</CommandTable>
@@ -274,7 +676,7 @@ sim tables exports create <tableId> [options]
## Get table export
```bash
sim tables exports get <exportId>
sim tables exports get <tableId> <exportId>
```
**Arguments**
@@ -283,6 +685,7 @@ sim tables exports get <exportId>
| Argument | Required | Description |
| --- | --- | --- |
| `tableId` | Yes | Unique table identifier. |
| `exportId` | Yes | Unique table-export identifier. |
</CommandTable>
@@ -290,7 +693,7 @@ sim tables exports get <exportId>
## Get the download URL for a finished export
```bash
sim tables exports download <exportId>
sim tables exports download <tableId> <exportId>
```
**Arguments**
@@ -299,6 +702,7 @@ sim tables exports download <exportId>
| Argument | Required | Description |
| --- | --- | --- |
| `tableId` | Yes | Unique table identifier. |
| `exportId` | Yes | Unique table-export identifier. |
</CommandTable>
@@ -361,6 +765,7 @@ sim tables cancel-runs <tableId> [options]
| `--row-id <value>` | No | Row whose runs should be canceled for row scope. |
| `--filter <json\|@file>` | No | Predicate: &#123;"all":[&#123;"field":"status","op":"eq","value":"active"&#125;]&#125;; groups use all/any. Operators: eq, ne, gt, gte, lt, lte, in, nin, contains, ncontains, startsWith, endsWith, like, ilike, nlike, nilike, isEmpty, isNotEmpty, isNull, isNotNull (JSON, or @path / @- to read a file or stdin). |
| `--exclude-row-ids <value...>` | No | Rows excluded from an all-scope cancellation. (space-separated, or @path / @- with one value per line). |
| `-y, --yes` | Yes | Confirm this destructive operation. |
</CommandTable>
@@ -466,10 +871,10 @@ Also available as `sim tables folders mv`.
</CommandTable>
## Create rows
## Restore an archived table folder
```bash
sim tables rows create <tableId> [options]
sim tables folders restore <path>
```
**Arguments**
@@ -478,273 +883,7 @@ sim tables rows create <tableId> [options]
| Argument | Required | Description |
| --- | --- | --- |
| `tableId` | Yes | Unique table identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--data <json\|@file>` | No | One row keyed by column name (JSON, or @path / @-; choose exactly one body flag). |
| `--rows <json\|@file>` | No | Several rows keyed by column name (JSON, or @path / @-; choose exactly one body flag). |
</CommandTable>
## Delete row
```bash
sim tables rows delete <tableId> <rowId> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `tableId` | Yes | Unique table identifier. |
| `rowId` | Yes | Unique table row identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `-y, --yes` | Yes | Confirm this destructive operation. |
</CommandTable>
## Delete rows matching a filter, or an explicit list of ids
```bash
sim tables rows batch-delete <tableId> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `tableId` | Yes | Unique table identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--filter <json\|@file>` | No | Predicate: &#123;"all":[&#123;"field":"status","op":"eq","value":"active"&#125;]&#125;; groups use all/any. Operators: eq, ne, gt, gte, lt, lte, in, nin, contains, ncontains, startsWith, endsWith, like, ilike, nlike, nilike, isEmpty, isNotEmpty, isNull, isNotNull (JSON, or @path / @- to read a file or stdin). |
| `--limit <n>` | No | Maximum items to return (0 for everything). Defaults to `100`. |
| `--row <value...>` | No | Explicit row identifiers to delete. (space-separated, or @path / @- with one value per line). |
| `-y, --yes` | Yes | Confirm this destructive operation. |
</CommandTable>
## Find rows matching a predicate
```bash
sim tables rows find <tableId> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `tableId` | Yes | Unique table identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--query <value>` | Yes | Value to find. |
| `--filter <json\|@file>` | No | Predicate: &#123;"all":[&#123;"field":"status","op":"eq","value":"active"&#125;]&#125;; groups use all/any. Operators: eq, ne, gt, gte, lt, lte, in, nin, contains, ncontains, startsWith, endsWith, like, ilike, nlike, nilike, isEmpty, isNotEmpty, isNull, isNotNull (JSON, or @path / @- to read a file or stdin). |
| `--sort <json\|@file>` | No | Ordered sort keys: [&#123;"field":"createdAt","direction":"desc"&#125;] (direction: asc or desc) (JSON, or @path / @- to read a file or stdin). |
</CommandTable>
## Get row
```bash
sim tables rows get <tableId> <rowId>
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `tableId` | Yes | Unique table identifier. |
| `rowId` | Yes | Unique table row identifier. |
</CommandTable>
## List rows
```bash
sim tables rows list <tableId> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `tableId` | Yes | Unique table identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--limit <n>` | No | Maximum items to return (0 for everything). Defaults to `100`. |
</CommandTable>
## Query rows
```bash
sim tables rows query <tableId> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `tableId` | Yes | Unique table identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--filter <json\|@file>` | No | Condition: &#123;"field":"status","op":"eq","value":"active"&#125;. Groups: &#123;"all":[&#123;"field":"status","op":"eq","value":"active"&#125;]&#125; or &#123;"any":[&#123;"field":"status","op":"eq","value":"active"&#125;]&#125;; group entries may also be nested groups. Operators: eq, ne, gt, gte, lt, lte, in, nin, contains, ncontains, startsWith, endsWith, like, ilike, nlike, nilike, isEmpty, isNotEmpty, isNull, isNotNull (JSON, or @path / @- to read a file or stdin). |
| `--sort <json\|@file>` | No | Ordered sort keys: [&#123;"field":"createdAt","direction":"desc"&#125;] (direction: asc or desc) (JSON, or @path / @- to read a file or stdin). |
| `--limit <n>` | No | Maximum items to return (0 for everything). Defaults to `100`. |
</CommandTable>
## Count rows matching a filter
```bash
sim tables rows count <tableId> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `tableId` | Yes | Unique table identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--filter <json\|@file>` | No | Condition: &#123;"field":"status","op":"eq","value":"active"&#125;. Groups: &#123;"all":[&#123;"field":"status","op":"eq","value":"active"&#125;]&#125; or &#123;"any":[&#123;"field":"status","op":"eq","value":"active"&#125;]&#125;; group entries may also be nested groups. Operators: eq, ne, gt, gte, lt, lte, in, nin, contains, ncontains, startsWith, endsWith, like, ilike, nlike, nilike, isEmpty, isNotEmpty, isNull, isNotNull (JSON, or @path / @- to read a file or stdin). |
</CommandTable>
## Run one rows enrichment group
```bash
sim tables rows enrich <tableId> <rowId> <groupId>
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `tableId` | Yes | Unique table identifier. |
| `rowId` | Yes | Unique table row identifier. |
| `groupId` | Yes | Workflow or enrichment group to run. |
</CommandTable>
## Update every row matching a filter
```bash
sim tables rows batch-update <tableId> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `tableId` | Yes | Unique table identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--filter <json\|@file>` | Yes | Predicate: &#123;"all":[&#123;"field":"status","op":"eq","value":"active"&#125;]&#125;; groups use all/any. Operators: eq, ne, gt, gte, lt, lte, in, nin, contains, ncontains, startsWith, endsWith, like, ilike, nlike, nilike, isEmpty, isNotEmpty, isNull, isNotNull (JSON, or @path / @- to read a file or stdin). |
| `--data <json\|@file>` | Yes | Row-data patch applied to every matching row. (JSON, or @path / @- to read a file or stdin). |
| `--limit <n>` | No | Maximum items to return (0 for everything). Defaults to `100`. |
| `-y, --yes` | Yes | Confirm this destructive operation. |
</CommandTable>
## Update row
```bash
sim tables rows update <tableId> <rowId> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `tableId` | Yes | Unique table identifier. |
| `rowId` | Yes | Unique table row identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--data <json\|@file>` | Yes | Partial row-data patch keyed by column name. (JSON, or @path / @- to read a file or stdin). |
| `path` | Yes | Folder path as shown in the app; the leading / is optional |
</CommandTable>
@@ -892,6 +1031,24 @@ sim tables delete <tableId> [options]
</CommandTable>
## Get enrichment run detail
```bash
sim tables enrichment get <tableId> <rowId> <groupId>
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `tableId` | Yes | Unique table identifier. |
| `rowId` | Yes | Unique table row identifier. |
| `groupId` | Yes | Workflow or enrichment group to run. |
</CommandTable>
## Get table
```bash
@@ -920,6 +1077,7 @@ sim tables list [options]
| Option | Required | Description |
| --- | --- | --- |
| `--scope <value>` | No | Which lifecycle set to list: `active` (default) for live tables, `archived` for tables a `DELETE` archived and `POST /tables/&#123;tableId&#125;/restore` can bring back. `folderPath` resolves against active folders only, so pairing it with `scope=archived` returns an empty page when the containing folder was archived too. Accepted values: `active`, `archived`. |
| `--folder <value>` | No | Folder path as shown in the app; the leading / is optional. |
| `--search <value>` | No | Case-insensitive substring match against the resource name. |
| `--sort-by <value>` | No | Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order. Accepted values: `name`, `createdAt`, `updatedAt`. |
@@ -928,6 +1086,40 @@ sim tables list [options]
</CommandTable>
## Move tables and folders
```bash
sim tables move [options]
```
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--table-ids <json\|@file>` | No | Tables to move, by identifier. (JSON, or @path / @- to read a file or stdin). |
| `--folder <value...>` | No | Table folders to move, by path as shown in the app; the leading / is optional (space-separated, or @path / @- with one value per line). |
| `--to <value>` | No | Destination folder path; omit for root. |
</CommandTable>
## Restore an archived table
```bash
sim tables restore <tableId>
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `tableId` | Yes | Unique table identifier. |
</CommandTable>
## Update table
```bash
@@ -951,7 +1143,7 @@ sim tables update <tableId> [options]
| Option | Required | Description |
| --- | --- | --- |
| `--name <value>` | No | Identifier: letters, numbers, and underscores; cannot start with a number. |
| `--description <value>` | No | Replacement table description, or null to clear it. |
| `--description <value>` | No | Replacement table description, or null to clear it. (--description null sends the word, not JSON null). |
| `--folder <value>` | No | Folder path as shown in the app; the leading / is optional. |
</CommandTable>
+45
View File
@@ -0,0 +1,45 @@
---
title: Tools
description: Manage tools — every subcommand, argument, and flag
---
import { CommandTable } from '@/components/ui/command-table'
Every command below also accepts the [global options](/cli/commands#global-options).
## Get tool
```bash
sim tools get <toolId>
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `toolId` | Yes | Tool identifier. An unversioned name resolves to the newest version, and the response echoes the resolved id. |
</CommandTable>
## List tools
```bash
sim tools list [options]
```
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--search <value>` | No | Case-insensitive substring match against the tool id, name, and description. |
| `--hosted-api-key <value>` | No | Restrict to tools by how their API key is supplied. Accepted values: `always`, `conditional`, `none`. |
| `--oauth-provider <value>` | No | Restrict to tools that authenticate against this OAuth service. |
| `--sort-by <value>` | No | Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order. Accepted values: `id`, `name`. |
| `--sort-order <value>` | No | Sort direction. Accepted values: `asc`, `desc`. |
| `--limit <n>` | No | Maximum items to return (0 for everything). Defaults to `100`. |
</CommandTable>
@@ -0,0 +1,189 @@
---
title: Workflow Mcp Servers
description: Manage workflow mcp servers — every subcommand, argument, and flag
---
import { CommandTable } from '@/components/ui/command-table'
Every command below also accepts the [global options](/cli/commands#global-options).
## Create workflow MCP server
```bash
sim workflow-mcp-servers create [options]
```
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--name <value>` | Yes | Server display name, shown to connecting MCP clients. |
| `--description <value>` | No | Optional server description. |
| `--is-public` | No | Whether the server answers MCP clients without a Sim API key. Defaults to false — a public server executes the workflows it publishes for anyone holding its URL. |
| `--no-is-public` | No | Send --is-public as false. |
| `--workflow <value...>` | No | Deployed workflows to publish as tools on the new server. (space-separated, or @path / @- with one value per line). |
</CommandTable>
## Delete workflow MCP server
```bash
sim workflow-mcp-servers delete <serverId> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `serverId` | Yes | Unique workflow-MCP server identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `-y, --yes` | Yes | Confirm this destructive operation. |
</CommandTable>
## Publish workflow as MCP tool
```bash
sim workflow-mcp-servers tools create <serverId> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `serverId` | Yes | Unique workflow-MCP server identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--workflow-id <value>` | Yes | Deployed workflow to publish. The workflow must already be deployed. |
| `--tool-name <value>` | No | Name MCP clients call. Normalized to the MCP tool-name grammar, and derived from the workflow name when omitted. |
| `--tool-description <value>` | No | Description shown to MCP clients. Derived from the workflow name when omitted. |
| `--parameter-descriptions <json\|@file>` | No | Per-field description overrides applied to the schema generated from the deployed workflow inputs, as [&#123;"name":"email","description":"Customer email address"&#125;]. A name matching no input field is ignored (JSON, or @path / @- to read a file or stdin). |
</CommandTable>
## List workflow MCP tools
```bash
sim workflow-mcp-servers tools list <serverId>
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `serverId` | Yes | Unique workflow-MCP server identifier. |
</CommandTable>
## Unpublish workflow MCP tool
```bash
sim workflow-mcp-servers tools delete <serverId> <workflowId> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `serverId` | Yes | Unique workflow-MCP server identifier. |
| `workflowId` | Yes | Workflow published as a tool on this server. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `-y, --yes` | Yes | Confirm this destructive operation. |
</CommandTable>
## Get workflow MCP server
```bash
sim workflow-mcp-servers get <serverId>
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `serverId` | Yes | Unique workflow-MCP server identifier. |
</CommandTable>
## List workflow MCP servers
```bash
sim workflow-mcp-servers list [options]
```
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--sort-by <value>` | No | Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order. Accepted values: `name`, `createdAt`, `updatedAt`. |
| `--sort-order <value>` | No | Sort direction. Accepted values: `asc`, `desc`. |
| `--limit <n>` | No | Maximum items to return (0 for everything). Defaults to `100`. |
</CommandTable>
## Update workflow MCP server
```bash
sim workflow-mcp-servers update <serverId> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `serverId` | Yes | Unique workflow-MCP server identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--name <value>` | No | Server display name, shown to connecting MCP clients. |
| `--description <value>` | No | New server description, or null to clear it. (--description null sends the word, not JSON null). |
| `--is-public` | No | Whether the server answers MCP clients without a Sim API key. |
| `--no-is-public` | No | Send --is-public as false. |
</CommandTable>
+398 -30
View File
@@ -9,6 +9,83 @@ import { CommandTable } from '@/components/ui/command-table'
Every command below also accepts the [global options](/cli/commands#global-options).
## Activate workflow version
```bash
sim workflows activate create <workflowId> <version>
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `workflowId` | Yes | Unique workflow identifier. |
| `version` | Yes | Numeric deployment version. |
</CommandTable>
## Apply workflow operations
```bash
sim workflows operations apply <workflowId> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `workflowId` | Yes | Unique workflow identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--dry-run` | No | Validate and lint without persisting. The response is identical to the committed write of the same body, so a caller can inspect `lint` and then re-send the request for real. Nothing is written, no audit entry is recorded, and collaborators are not notified. |
| `--no-dry-run` | No | Send --dry-run as false. |
| `--operations <json\|@file>` | Yes | Edits to apply, in a single batch, keyed by operation_type: [&#123;"operation_type":"add","block_id":"my-fn","params":&#123;"type":"function","name":"My Fn","inputs":&#123;"code":"return &#123;ok:true&#125;"&#125;&#125;&#125;,&#123;"operation_type":"edit","block_id":"&lt;uuid&gt;","params":&#123;"name":"Renamed","connections":&#123;"success":"my-fn"&#125;&#125;&#125;,&#123;"operation_type":"delete","block_id":"&lt;uuid&gt;"&#125;]. Also insert_into_subflow and extract_from_subflow, whose params carry &#123;"subflowId":"&lt;loop-id&gt;"&#125; (JSON, or @path / @- to read a file or stdin). |
| `--atomic` | No | Fail the whole batch when any operation is declined or any block input would be dropped. The default applies what it can and reports the rest in `skipped` and `inputValidationErrors`; `true` writes nothing and answers `409` instead. |
| `--no-atomic` | No | Send --atomic as false. |
| `--layout <value>` | No | Whether to reposition blocks the batch touched. `targeted` (default) nudges only the affected subgraph; `none` leaves every position exactly as supplied. Accepted values: `targeted`, `none`. |
| `--set-block-enabled <json\|@file>` | No | Blocks to enable or disable, applied after --operations: [&#123;"block_id":"&lt;uuid&gt;","enabled":false&#125;]. Disabling a loop or parallel cascades to its unlocked descendants; enabling a block whose container is disabled is declined (JSON, or @path / @- to read a file or stdin). |
| `-y, --yes` | No | Confirm this destructive operation (required unless --dry-run). |
</CommandTable>
## Update workflow variables
```bash
sim workflows variables update <workflowId> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `workflowId` | Yes | Unique workflow identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--operations <json\|@file>` | Yes | Variable changes to apply in order, keyed by operation: [&#123;"operation":"add","name":"my_var","type":"string","value":"hello"&#125;,&#123;"operation":"edit","name":"my_var","value":"updated"&#125;,&#123;"operation":"delete","name":"my_var"&#125;] (JSON, or @path / @- to read a file or stdin). |
| `-y, --yes` | Yes | Confirm this destructive operation. |
</CommandTable>
## Cancel a running workflow run
```bash
@@ -62,6 +139,9 @@ Show run status (requested outputs are included in JSON or YAML output)
| `--workflow <workflowId>` | Yes | Workflow ID. |
| `--include-output` | No | Include the final output in JSON or YAML output. |
| `--select-output <value...>` | No | Include blockName.field values in JSON or YAML output (e.g. agent_1.content) (space-separated, or @path / @- with one value per line). |
| `--include-file-base64` | No | Inline each produced file's bytes as base64. Requires `includeOutput`. A file above the inline ceiling answers `413` naming its download path; fetch large files from `downloadPath` instead. |
| `--no-include-file-base64` | No | Send --include-file-base64 as false. |
| `--base64-max-bytes <value>` | No | Per-file inline ceiling, lowering but never raising the server limit of 16 MiB. |
</CommandTable>
@@ -248,7 +328,7 @@ Also available as `sim workflows folders mv`.
## Delete workflow
```bash
sim workflows delete <id> [options]
sim workflows delete <workflowId> [options]
```
**Arguments**
@@ -257,7 +337,7 @@ sim workflows delete <id> [options]
| Argument | Required | Description |
| --- | --- | --- |
| `id` | Yes | Unique workflow identifier. |
| `workflowId` | Yes | Unique workflow identifier. |
</CommandTable>
@@ -271,10 +351,10 @@ sim workflows delete <id> [options]
</CommandTable>
## Deploy workflow
## Take a workflows chat deployment offline
```bash
sim workflows deploy <id> [options]
sim workflows chat unpublish <workflowId> [options]
```
**Arguments**
@@ -283,7 +363,87 @@ sim workflows deploy <id> [options]
| Argument | Required | Description |
| --- | --- | --- |
| `id` | Yes | Unique workflow identifier. |
| `workflowId` | Yes | Unique workflow identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `-y, --yes` | Yes | Confirm this destructive operation. |
</CommandTable>
## Show a workflows chat deployment
```bash
sim workflows chat status <workflowId>
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `workflowId` | Yes | Unique workflow identifier. |
</CommandTable>
## Publish or replace a workflows chat deployment
```bash
sim workflows chat publish <workflowId> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `workflowId` | Yes | Unique workflow identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--identifier <value>` | Yes | URL slug the deployed chat answers on. Must be free across live deployments. |
| `--title <value>` | Yes | Title shown to visitors. |
| `--description <value>` | No | Description shown to visitors. Omitted clears it. |
| `--customizations <json\|@file>` | No | Presentation overrides. Omitted fields take platform defaults. (JSON, or @path / @- to read a file or stdin). |
| `--auth-type <value>` | No | How visitors are gated. `public` leaves the chat open to anyone holding the URL. Accepted values: `public`, `password`, `email`, `sso`. |
| `--password <value>` | No | Write-only password. Required whenever `authType` is `password`, and rejected otherwise. Never readable back. |
| `--allowed-emails <json\|@file>` | No | Email addresses or domains admitted under `email` and `sso` gating. At least one is required for those modes. (JSON, or @path / @- to read a file or stdin). |
| `--output-configs <json\|@file>` | No | Block outputs to surface to visitors. Omitted surfaces none. (JSON, or @path / @- to read a file or stdin). |
| `--include-thinking` | No | Allow visitors to receive provider thinking events. |
| `--no-include-thinking` | No | Send --include-thinking as false. |
| `--include-tool-calls` | No | Allow visitors to receive tool lifecycle events. |
| `--no-include-tool-calls` | No | Send --include-tool-calls as false. |
| `-y, --yes` | Yes | Confirm this destructive operation. |
</CommandTable>
## Deploy workflow
```bash
sim workflows deploy <workflowId> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `workflowId` | Yes | Unique workflow identifier. |
</CommandTable>
@@ -298,10 +458,10 @@ sim workflows deploy <id> [options]
</CommandTable>
## Run a deployed workflow
## Duplicate workflow
```bash
sim workflows run <id> [options]
sim workflows duplicate create <workflowId> [options]
```
**Arguments**
@@ -310,7 +470,34 @@ sim workflows run <id> [options]
| Argument | Required | Description |
| --- | --- | --- |
| `id` | Yes | Unique workflow identifier. |
| `workflowId` | Yes | Unique workflow identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--name <value>` | No | Name for the copy. Defaults to the source name, deduplicated within the folder. |
| `--folder <value>` | No | Folder path as shown in the app; the leading / is optional. |
</CommandTable>
## Run a deployed workflow or execute saved state manually
```bash
sim workflows run <workflowId> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `workflowId` | Yes | Unique workflow identifier. |
</CommandTable>
@@ -326,7 +513,13 @@ sim workflows run <id> [options]
| `--select-output <value...>` | No | Return blockName.field values (e.g. agent_1.content); missing fields are omitted (space-separated, or @path / @- with one value per line). |
| `--include-file-base64` | No | Inline eligible output files as base64 content. Rejected when `async` is true. |
| `--no-include-file-base64` | No | Send --include-file-base64 as false. |
| `--base64-max-bytes <value>` | No | Maximum total bytes of file content to inline as base64. Rejected when `async` is true. |
| `--base64-max-bytes <value>` | No | Maximum total bytes of file content to inline as base64, lowering but never raising the server limit of 16 MiB. Rejected when `async` is true. |
| `--run-id <value>` | No | One-shot identifier for this run; NOT an idempotency key — reusing a claimed value fails with RUN_ID_CONFLICT instead of replaying the first result, and a fresh value starts another run. |
| `--manual` | No | Run the current saved workflow state instead of the active deployment. |
| `--trigger <blockId>` | No | Enter a manual run through this runnable trigger (requires --manual). |
| `--mock-payload` | No | Use the selected trigger's server-derived mock payload (requires --manual). |
| `--from-block <blockId>` | No | Run manually from this saved workflow block. |
| `--source-run <runId>` | No | Prior run whose persisted state supplies upstream outputs (requires --from-block). |
| `--follow` | No | Stream the run as it happens; progress on stderr, result on stdout. The stream reports only success and output, so the result omits the run id and timings a non-streaming run returns. |
| `--include-thinking` | No | Show model reasoning while following (requires --follow). |
| `--include-tool-calls` | No | Show tool calls while following (requires --follow). |
@@ -336,7 +529,7 @@ sim workflows run <id> [options]
## Print a workflow as a portable JSON document
```bash
sim workflows export <id>
sim workflows export <workflowId>
```
**Arguments**
@@ -345,14 +538,14 @@ sim workflows export <id>
| Argument | Required | Description |
| --- | --- | --- |
| `id` | Yes | Unique workflow identifier. |
| `workflowId` | Yes | Unique workflow identifier. |
</CommandTable>
## Get workflow
```bash
sim workflows get <id>
sim workflows get <workflowId>
```
**Arguments**
@@ -361,14 +554,14 @@ sim workflows get <id>
| Argument | Required | Description |
| --- | --- | --- |
| `id` | Yes | Unique workflow identifier. |
| `workflowId` | Yes | Unique workflow identifier. |
</CommandTable>
## Show a workflows current deployment
```bash
sim workflows deployment status <id>
sim workflows deployment status <workflowId>
```
**Arguments**
@@ -377,14 +570,89 @@ sim workflows deployment status <id>
| Argument | Required | Description |
| --- | --- | --- |
| `id` | Yes | Unique workflow identifier. |
| `workflowId` | Yes | Unique workflow identifier. |
</CommandTable>
## Update workflow public API access
```bash
sim workflows deployment update <workflowId> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `workflowId` | Yes | Unique workflow identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--is-public-api <true\|false>` | Yes | Whether the deployed workflow should accept unauthenticated public API execution. Accepted values: `true`, `false`. |
</CommandTable>
## Get workflow state
```bash
sim workflows state get <workflowId>
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `workflowId` | Yes | Unique workflow identifier. |
</CommandTable>
## Replace workflow state
```bash
sim workflows state replace <workflowId> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `workflowId` | Yes | Unique workflow identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--dry-run` | No | Validate and lint without persisting. The response is identical to the committed write of the same body, so a caller can inspect `lint` and then re-send the request for real. Nothing is written, no audit entry is recorded, and collaborators are not notified. |
| `--no-dry-run` | No | Send --dry-run as false. |
| `--blocks <json\|@file>` | Yes | Blocks keyed by block id. (JSON, or @path / @- to read a file or stdin). |
| `--edges <json\|@file>` | Yes | Directed connections between blocks. (JSON, or @path / @- to read a file or stdin). |
| `--loops <json\|@file>` | No | Ignored on write: loop containers are recomputed from `blocks`. (JSON, or @path / @- to read a file or stdin). |
| `--parallels <json\|@file>` | No | Ignored on write: parallel containers are recomputed from `blocks`. (JSON, or @path / @- to read a file or stdin). |
| `--variables <json\|@file>` | No | Replacement variable set. Omit to leave the stored variables untouched. (JSON, or @path / @- to read a file or stdin). |
| `-y, --yes` | No | Confirm this destructive operation (required unless --dry-run). |
</CommandTable>
## Get workflow version
```bash
sim workflows versions get <id> <version>
sim workflows versions get <workflowId> <version>
```
**Arguments**
@@ -393,7 +661,7 @@ sim workflows versions get <id> <version>
| Argument | Required | Description |
| --- | --- | --- |
| `id` | Yes | Unique workflow identifier. |
| `workflowId` | Yes | Unique workflow identifier. |
| `version` | Yes | Numeric deployment version. |
</CommandTable>
@@ -401,7 +669,7 @@ sim workflows versions get <id> <version>
## List workflow versions
```bash
sim workflows versions list <id> [options]
sim workflows versions list <workflowId> [options]
```
**Arguments**
@@ -410,7 +678,7 @@ sim workflows versions list <id> [options]
| Argument | Required | Description |
| --- | --- | --- |
| `id` | Yes | Unique workflow identifier. |
| `workflowId` | Yes | Unique workflow identifier. |
</CommandTable>
@@ -424,6 +692,34 @@ sim workflows versions list <id> [options]
</CommandTable>
## Update workflow version
```bash
sim workflows versions update <workflowId> <version> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `workflowId` | Yes | Unique workflow identifier. |
| `version` | Yes | Numeric deployment version. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--name <value>` | No | New label for the deployment version. |
| `--description <value>` | No | New release note for the deployment version, or null to clear it. (--description null sends the word, not JSON null). |
</CommandTable>
## Import workflow
```bash
@@ -455,6 +751,7 @@ sim workflows list [options]
| Option | Required | Description |
| --- | --- | --- |
| `--scope <value>` | No | Which lifecycle set to list: `active` (default) for live workflows, `archived` for workflows a `DELETE` archived. `folderPath` resolves against active folders only, so pairing it with `scope=archived` returns an empty page when the containing folder was archived too. Accepted values: `active`, `archived`. |
| `--folder <value>` | No | Folder path as shown in the app; the leading / is optional. |
| `--deployed-only` | No | Return only workflows with an active deployment when true. |
| `--no-deployed-only` | No | Send --deployed-only as false. |
@@ -465,10 +762,27 @@ sim workflows list [options]
</CommandTable>
## Rollback workflow
## Move workflows
```bash
sim workflows rollback <id> [options]
sim workflows move [options]
```
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--workflow <value...>` | Yes | Workflows to move. Duplicates are collapsed. (space-separated, or @path / @- with one value per line). |
| `--to <value>` | Yes | Destination folder path; / moves the workflows to the workspace root. |
</CommandTable>
## Restore an archived workflow
```bash
sim workflows restore <workflowId>
```
**Arguments**
@@ -477,7 +791,24 @@ sim workflows rollback <id> [options]
| Argument | Required | Description |
| --- | --- | --- |
| `id` | Yes | Unique workflow identifier. |
| `workflowId` | Yes | Unique workflow identifier. |
</CommandTable>
## Revert workflow to version
```bash
sim workflows revert create <workflowId> <version> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `workflowId` | Yes | Unique workflow identifier. |
| `version` | Yes | Numeric deployment version, or `active` for the currently live version. |
</CommandTable>
@@ -487,14 +818,41 @@ sim workflows rollback <id> [options]
| Option | Required | Description |
| --- | --- | --- |
| `--version <value>` | No | Deployment version to reactivate. Omit to select the previous active version. |
| `-y, --yes` | Yes | Confirm this destructive operation. |
</CommandTable>
## Rollback workflow
```bash
sim workflows rollback <workflowId> [options]
```
**Arguments**
<CommandTable>
| Argument | Required | Description |
| --- | --- | --- |
| `workflowId` | Yes | Unique workflow identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `--to-version <value>` | No | Deployment version to reactivate. Omit to select the previous active version. |
| `-y, --yes` | Yes | Confirm this destructive operation. |
</CommandTable>
## Take a workflow out of deployment
```bash
sim workflows undeploy <id>
sim workflows undeploy <workflowId> [options]
```
**Arguments**
@@ -503,14 +861,24 @@ sim workflows undeploy <id>
| Argument | Required | Description |
| --- | --- | --- |
| `id` | Yes | Unique workflow identifier. |
| `workflowId` | Yes | Unique workflow identifier. |
</CommandTable>
**Options**
<CommandTable>
| Option | Required | Description |
| --- | --- | --- |
| `-y, --yes` | Yes | Confirm this destructive operation. |
</CommandTable>
## Update workflow
```bash
sim workflows update <id> [options]
sim workflows update <workflowId> [options]
```
**Arguments**
@@ -519,7 +887,7 @@ sim workflows update <id> [options]
| Argument | Required | Description |
| --- | --- | --- |
| `id` | Yes | Unique workflow identifier. |
| `workflowId` | Yes | Unique workflow identifier. |
</CommandTable>
@@ -530,7 +898,7 @@ sim workflows update <id> [options]
| Option | Required | Description |
| --- | --- | --- |
| `--name <value>` | No | Replacement workflow name. |
| `--description <value>` | No | Replacement workflow description; null clears it. |
| `--description <value>` | No | Replacement workflow description; null clears it. (--description null sends the word, not JSON null). |
| `--folder <value>` | No | Folder path as shown in the app; the leading / is optional. |
</CommandTable>
@@ -538,7 +906,7 @@ sim workflows update <id> [options]
## Move a workflow to a folder
```bash
sim workflows mv <id> <folder>
sim workflows mv <workflowId> <folder>
```
**Arguments**
@@ -547,7 +915,7 @@ sim workflows mv <id> <folder>
| Argument | Required | Description |
| --- | --- | --- |
| `id` | Yes | Unique workflow identifier. |
| `workflowId` | Yes | Unique workflow identifier. |
| `folder` | Yes | Folder path as shown in the app; the leading / is optional |
</CommandTable>
@@ -32,7 +32,7 @@ Integrate Enrow to find verified B2B email addresses from a full name and compan
### Enrow Find Email
Find a verified B2B email address from a full name and company domain or name. Uses the Enrow async finder — submits a search and polls until the result is ready. Costs 1 credit per valid email found. (https://enrow.readme.io/reference/find-single-email)
Find a verified B2B email address from a full name and company domain or name. Uses the Enrow async finder — submits a search and polls until the result is ready. Costs 1 credit per valid email found. (https://docs.enrow.io/api-reference/email-finder/find-single)
#### Input
@@ -51,13 +51,14 @@ Find a verified B2B email address from a full name and company domain or name. U
| `email` | string | Email address found or verified |
| `qualification` | string | Enrow quality result: "valid" or "invalid" |
| `fullname` | string | Full name of the person searched |
| `firstname` | string | First name of the person searched |
| `lastname` | string | Last name of the person searched |
| `company_name` | string | Company name associated with the result |
| `company_domain` | string | Company domain associated with the result |
| `linkedin_url` | string | LinkedIn profile URL of the person |
### Enrow Verify Email
Verify the deliverability of an email address using the Enrow async verifier. Submits a verification request and polls until the result is ready. Costs 0.25 credits per verification. (https://enrow.readme.io/reference/verify-single-email)
Verify the deliverability of an email address using the Enrow async verifier. Submits a verification request and polls until the result is ready. Costs 0.25 credits per verification. (https://docs.enrow.io/api-reference/email-verifier/verify-single)
#### Input
@@ -163,6 +163,7 @@
"microsoft_excel",
"microsoft_planner",
"microsoft_teams",
"microsoft_word",
"millionverifier",
"mintlify",
"mistral_parse",
@@ -221,6 +222,7 @@
"sap_concur",
"sap_s4hana",
"secrets_manager",
"semrush",
"sendblue",
"sendgrid",
"sentry",
@@ -0,0 +1,286 @@
---
title: Microsoft Word
description: Create, fill, read, edit, and export Word documents
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="microsoft_word"
color="#FFFFFF"
/>
{/* MANUAL-CONTENT-START:intro */}
[Microsoft Word](https://www.microsoft.com/microsoft-365/word) is the document editor at the centre of Microsoft 365. Word documents are stored as `.docx` files in OneDrive or a SharePoint document library, which is where this integration reads and writes them.
Learn how to integrate the Microsoft Word tool in Sim to generate, fill, edit, and export documents inside your workflows. This tutorial walks you through connecting a Microsoft account, choosing a document, and turning agent output into a finished Word file. Ideal for the document-heavy work a team would otherwise do by hand.
With Microsoft Word, you can:
- **Write documents from agent output**: Turn generated text into a real `.docx` with headings, bullets, and bold or italic emphasis
- **Fill a template**: Substitute placeholders in an approved template to produce a contract, offer letter, or report — the template itself is never modified
- **Read a document back**: Extract the text of any Word document so an agent can summarize, classify, or check it
- **Edit in place**: Append new paragraphs to a running document, or find and replace text across the body plus every header and footer
- **Find the right file**: List or search Word documents in a folder, a whole drive, or a SharePoint library
- **Hand over a PDF**: Convert a document to PDF through Microsoft Graph and pass the file straight to an email or messaging step
In Sim, the Microsoft Word integration lets your agents own a document end to end. An agent can find the standard template, fill it with data pulled from a CRM or a form, export the result as a PDF, and send it — without a person opening Word. Because the integration edits the `.docx` package directly rather than regenerating it, an append or a find-and-replace leaves the document's existing styles, images, headers, and footers exactly as they were.
### How documents are addressed
Every operation identifies a document by its **drive item ID**, not by its file path. Pick one with the document selector, or paste an ID from an earlier step. Leave **Drive ID** empty to work in your personal OneDrive; set it to a SharePoint drive ID to work in a document library instead.
### Writing content
**Create Document** and **Replace Content** accept a small, predictable subset of Markdown:
| You write | Word renders |
| --------- | ------------ |
| `# Heading`, `## Heading`, `### Heading` | Heading 1, 2, and 3 |
| `- item` | A bulleted list item |
| `**bold**`, `*italic*` | Bold and italic text |
| Anything else | A plain paragraph |
Unsupported Markdown is left as visible text rather than being dropped, so nothing goes missing silently.
**Replace Content** overwrites the whole document — its previous content and formatting are discarded. To add to a document instead, use **Append Content**, which adds plain paragraphs to the end and leaves everything already there untouched.
### Filling templates
**Create from Template** is the fastest way to produce a formatted document. Point it at a template, give the new document a name, and supply **Placeholder Values** as a JSON object:
```json
{
"{{customer_name}}": "Acme Corp",
"{{effective_date}}": "2026-01-31",
"{{total}}": "$48,000"
}
```
Placeholders are matched as literal text — there is no pattern syntax — so you can use whatever delimiter your template already uses. Any placeholder missing from the template is simply ignored, and the number of substitutions made is returned as `occurrencesChanged`, which is worth checking: a count of `0` means the keys do not match what is actually in the template.
Word splits a sentence across several internal runs whenever formatting changes mid-line, so a placeholder like `{{customer_name}}` is often stored in pieces. Sim reassembles each paragraph before matching, so those placeholders are still found. When a match sits entirely inside one run, its formatting is preserved exactly; when a match straddles runs, that paragraph takes on the formatting of its first run.
**Find and Replace Text** applies the same matching to a document already in place, and covers headers and footers as well as the body. Matching never crosses a paragraph break, which mirrors Word's own Replace All.
### Permissions
Connecting a Microsoft account grants Sim `Files.Read` and `Files.ReadWrite`, the permissions Microsoft Graph requires to read and write drive items. The same permissions cover SharePoint document libraries the account can already reach — Sim never gains access to anything the signed-in account could not open itself.
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Integrate Microsoft Word into the workflow. Create .docx documents from text, fill a formatted template by substituting its placeholders, read a document back as text, replace or append content, find and replace text across the body plus headers and footers, list and search documents in OneDrive or SharePoint, and export a document as PDF.
## Actions
### Create Microsoft Word Document
Create a new Microsoft Word (.docx) document in OneDrive or SharePoint from text content. Supports Markdown headings (# ## ###), bullets (-), and inline **bold** / *italic*. An existing document with the same name is never overwritten — the new one is given a unique name instead.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `name` | string | Yes | The name of the document to create \(e.g., "Q3 Report"\). A .docx extension is added when missing. |
| `content` | string | No | The text content of the document. Markdown headings \(# ## ###\), bullets \(- item\), and inline **bold** / *italic* are converted to Word formatting; every other line becomes a paragraph. |
| `folderId` | string | No | The ID of the folder to create the document in. If omitted, the document is created in the drive root. |
| `driveId` | string | No | The ID of the drive to create the document in. Required for SharePoint. If omitted, uses the personal OneDrive. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `metadata` | object | Metadata for the created document |
| ↳ `documentId` | string | The drive item ID of the document |
| ↳ `name` | string | The document file name |
| ↳ `mimeType` | string | The document MIME type |
| ↳ `webViewLink` | string | Browser URL for opening the document |
| ↳ `size` | number | Document size in bytes |
| ↳ `createdTime` | string | ISO 8601 creation time |
| ↳ `modifiedTime` | string | ISO 8601 last modification time |
### Create Microsoft Word Document from Template
Copy an existing Microsoft Word (.docx) template to a new document and fill its placeholders. The template keeps all of its formatting, styles, headers, and footers, and is never modified. An existing document with the same name is never overwritten — the new one is given a unique name instead.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `templateDocumentId` | string | Yes | The drive item ID of the Word template to copy |
| `name` | string | Yes | The name of the document to create \(e.g., "Acme — Services Agreement"\). A .docx extension is added when missing. |
| `replacements` | json | No | A JSON object mapping each placeholder in the template to its value, e.g. \{"\{\{customer_name\}\}": "Acme Corp", "\{\{date\}\}": "2026-01-31"\}. Placeholders not present in the template are ignored. |
| `matchCase` | boolean | No | Whether placeholder matching is case-sensitive. Defaults to false. |
| `folderId` | string | No | The ID of the folder to create the new document in. If omitted, the drive root is used. |
| `driveId` | string | No | The ID of the drive holding the template and the new document. Required for SharePoint. If omitted, uses the personal OneDrive. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `occurrencesChanged` | number | How many placeholder occurrences were filled in the new document |
| `metadata` | object | Metadata for the newly created document |
| ↳ `documentId` | string | The drive item ID of the document |
| ↳ `name` | string | The document file name |
| ↳ `mimeType` | string | The document MIME type |
| ↳ `webViewLink` | string | Browser URL for opening the document |
| ↳ `size` | number | Document size in bytes |
| ↳ `createdTime` | string | ISO 8601 creation time |
| ↳ `modifiedTime` | string | ISO 8601 last modification time |
### Read Microsoft Word Document
Read the text content of a Microsoft Word (.docx) document stored in OneDrive or SharePoint.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `documentId` | string | Yes | The drive item ID of the Word document to read |
| `driveId` | string | No | The ID of the drive containing the document. Required for SharePoint. If omitted, uses the personal OneDrive. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | The extracted text content of the document |
| `metadata` | object | Metadata for the document that was read |
| ↳ `documentId` | string | The drive item ID of the document |
| ↳ `name` | string | The document file name |
| ↳ `mimeType` | string | The document MIME type |
| ↳ `webViewLink` | string | Browser URL for opening the document |
| ↳ `size` | number | Document size in bytes |
| ↳ `createdTime` | string | ISO 8601 creation time |
| ↳ `modifiedTime` | string | ISO 8601 last modification time |
### Update Microsoft Word Document
Replace the entire contents of an existing Microsoft Word (.docx) document with new text. The previous content and its formatting are discarded — use Append to add to a document instead.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `documentId` | string | Yes | The drive item ID of the Word document to replace |
| `content` | string | Yes | The new text content of the document. Markdown headings \(# ## ###\), bullets \(- item\), and inline **bold** / *italic* are converted to Word formatting; every other line becomes a paragraph. |
| `driveId` | string | No | The ID of the drive containing the document. Required for SharePoint. If omitted, uses the personal OneDrive. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `updatedContent` | boolean | Whether the document content was replaced |
| `metadata` | object | Metadata for the updated document |
| ↳ `documentId` | string | The drive item ID of the document |
| ↳ `name` | string | The document file name |
| ↳ `mimeType` | string | The document MIME type |
| ↳ `webViewLink` | string | Browser URL for opening the document |
| ↳ `size` | number | Document size in bytes |
| ↳ `createdTime` | string | ISO 8601 creation time |
| ↳ `modifiedTime` | string | ISO 8601 last modification time |
### Append to Microsoft Word Document
Append plain-text paragraphs to the end of an existing Microsoft Word (.docx) document, leaving the existing content and formatting intact. Fails rather than overwriting if someone else changed the document while the edit was in flight.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `documentId` | string | Yes | The drive item ID of the Word document to append to |
| `content` | string | Yes | The text to append. Each non-empty line becomes a paragraph at the end of the document. Markdown is not converted here — use Update to rewrite a document with formatting. |
| `driveId` | string | No | The ID of the drive containing the document. Required for SharePoint. If omitted, uses the personal OneDrive. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `updatedContent` | boolean | Whether the paragraphs were appended to the document |
| `metadata` | object | Metadata for the updated document |
| ↳ `documentId` | string | The drive item ID of the document |
| ↳ `name` | string | The document file name |
| ↳ `mimeType` | string | The document MIME type |
| ↳ `webViewLink` | string | Browser URL for opening the document |
| ↳ `size` | number | Document size in bytes |
| ↳ `createdTime` | string | ISO 8601 creation time |
| ↳ `modifiedTime` | string | ISO 8601 last modification time |
### Replace Text in Microsoft Word Document
Find and replace text throughout a Microsoft Word (.docx) document, including its headers and footers. Use this to fill placeholders in a template document. Fails rather than overwriting if someone else changed the document while the edit was in flight.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `documentId` | string | Yes | The drive item ID of the Word document to edit |
| `findText` | string | Yes | The literal text to find \(e.g., "\{\{customer_name\}\}"\). Matched as plain text, not a pattern, and never across a paragraph break. |
| `replaceText` | string | No | The text to substitute for each match. Omit to delete the matched text. |
| `matchCase` | boolean | No | Whether matching is case-sensitive. Defaults to false. |
| `driveId` | string | No | The ID of the drive containing the document. Required for SharePoint. If omitted, uses the personal OneDrive. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `occurrencesChanged` | number | How many occurrences of the search text were replaced |
| `metadata` | object | Metadata for the edited document |
| ↳ `documentId` | string | The drive item ID of the document |
| ↳ `name` | string | The document file name |
| ↳ `mimeType` | string | The document MIME type |
| ↳ `webViewLink` | string | Browser URL for opening the document |
| ↳ `size` | number | Document size in bytes |
| ↳ `createdTime` | string | ISO 8601 creation time |
| ↳ `modifiedTime` | string | ISO 8601 last modification time |
### List Microsoft Word Documents
List or search Microsoft Word (.docx) documents in OneDrive or SharePoint. Non-Word items are filtered out of the results.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `query` | string | No | Search text matched against file name, metadata, and content. If omitted, the documents directly inside the folder are listed. |
| `folderId` | string | No | The ID of the folder to list or search within. If omitted, the drive root is used. |
| `driveId` | string | No | The ID of the drive to list from. Required for SharePoint. If omitted, uses the personal OneDrive. |
| `pageSize` | number | No | Maximum number of items to request from Microsoft Graph \(1-200, default 50\) |
| `pageToken` | string | No | Continuation URL from a previous response's nextPageToken, used to fetch the next page |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `documents` | array | The Word documents that matched |
| ↳ `documentId` | string | The drive item ID of the document |
| ↳ `name` | string | The document file name |
| ↳ `mimeType` | string | The document MIME type |
| ↳ `webViewLink` | string | Browser URL for opening the document |
| ↳ `size` | number | Document size in bytes |
| ↳ `createdTime` | string | ISO 8601 creation time |
| ↳ `modifiedTime` | string | ISO 8601 last modification time |
| `nextPageToken` | string | Continuation URL for the next page of results, when more remain |
### Export Microsoft Word Document as PDF
Convert a Microsoft Word (.docx) document to PDF using Microsoft Graph and return it as a file.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `documentId` | string | Yes | The drive item ID of the Word document to convert |
| `fileName` | string | No | Optional name for the generated PDF \(e.g., "report.pdf"\). Defaults to the document name with a .pdf extension. |
| `driveId` | string | No | The ID of the drive containing the document. Required for SharePoint. If omitted, uses the personal OneDrive. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `file` | file | The converted PDF, stored in execution files |
File diff suppressed because it is too large Load Diff
@@ -45,14 +45,14 @@ A powerful web search tool that provides access to Google search results through
| `num` | number | No | Number of results to return \(e.g., 10, 20, 50\) |
| `gl` | string | No | Country code for search results \(e.g., "us", "uk", "de", "fr"\) |
| `hl` | string | No | Language code for search results \(e.g., "en", "es", "de", "fr"\) |
| `type` | string | No | Type of search to perform \(e.g., "search", "news", "images", "videos", "places", "shopping"\) |
| `type` | string | No | Type of search to perform. Must be one of "search", "news", "places", "images", "videos", "shopping", "scholar", "patents" — any other value is rejected. |
| `apiKey` | string | Yes | Serper API Key |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `searchResults` | array | Search results with titles, links, snippets, and type-specific metadata \(date for news, rating for places, imageUrl for images\) |
| `searchResults` | array | Search results with titles, links, snippets, and type-specific metadata \(date for news, rating for places, imageUrl for images, duration/source for videos, price/source for shopping\) |
| ↳ `title` | string | Result title |
| ↳ `link` | string | Result URL |
| ↳ `snippet` | string | Result description/snippet |
@@ -6,7 +6,7 @@ description: Send, update, delete messages, manage views and modals, add or remo
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="slack"
type="slack_v2"
color="#611f69"
/>
@@ -1872,18 +1872,27 @@ Set the purpose (description) for a Slack channel (max 250 characters).
A **Trigger** is a block that starts a workflow when an event happens in this service.
### Slack Webhook
### Slack
Trigger workflow from Slack events like mentions, messages, and reactions
Trigger from Slack events (mentions, messages, reactions)
#### Configuration
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `signingSecret` | string | Yes | The signing secret from your Slack app to validate request authenticity. |
| `botToken` | string | No | The bot token from your Slack app. Required for downloading files attached to messages. |
| `includeFiles` | boolean | No | Download and include file attachments from messages. Requires a bot token with files:read scope. |
| `setupWizard` | modal | No | Walk through manifest creation, app install, and pasting credentials. |
| `eventType` | string | Yes | The single Slack event this trigger fires on. Add another trigger block for another event. |
| `customBotCredential` | string | Yes | Choose a custom Slack bot you set up once and reuse across triggers. |
| `manualBotCredential` | string | Yes | Set the custom bot credential ID directly. |
| `source` | string | No | Restrict to direct messages, public channels, or private channels. Leave empty to match any. |
| `channelFilter` | channel-selector | No | Restrict to specific channels. Leave empty to trigger on any channel the bot has been added to. |
| `manualChannelFilter` | string | No | Comma-separated channel IDs to restrict to. Set IDs directly here. |
| `threads` | string | No | Include thread replies, exclude them \(top-level only\), or fire only on thread replies. |
| `emoji` | string | No | Comma-separated emoji names to restrict to. Leave empty to match any emoji. |
| `nameContains` | string | No | Only fire when the created channel name contains this text. |
| `interactionFilter` | string | No | Comma-separated action_ids \(buttons/selects\) or callback_ids \(modals\) to restrict to. Leave empty to fire on any interaction. |
| `filterBotMessages` | boolean | No | Ignore messages sent by other bots. This app's own output is always ignored. |
| `includeOwnMessages` | boolean | No | Also fire on this app's own messages and reactions. Can cause loops — use with care. |
| `includeFiles` | boolean | No | Download and include file attachments from messages. Requires files:read. |
#### Output
+12 -12
View File
@@ -331,27 +331,27 @@ Each voice session is billed when it starts. In deployed chat voice mode, each c
Sim has two paid plan tiers - **Pro** and **Max**. Either can be used individually or with a team. Team plans pool credits across all seats in the organization.
| Plan | Price | Credits Included | Daily Refresh |
|------|-------|------------------|---------------|
| Plan | Price | Credits Included | Weekly Refresh |
|------|-------|------------------|----------------|
| **Community** | $0 | 1,000 (one-time) | - |
| **Pro** | $25/mo | 6,000/mo | +50/day |
| **Max** | $100/mo | 25,000/mo | +200/day |
| **Pro** | $25/mo | 6,000/mo | +2,000/week |
| **Max** | $100/mo | 25,000/mo | +4,000/week |
| **Enterprise** | Custom | Custom | - |
To use Pro or Max with a team, select **Get For Team** in subscription settings and choose the tier and number of seats. Credits are pooled across the organization at the per-seat rate (e.g. Max for Teams with 3 seats = 75,000 credits/mo pooled).
Internal organization members use seats and contribute to the team's pooled credit allocation. External workspace members do not join your organization, do not appear in the organization roster, and do not count toward your seat total.
### Daily Refresh Credits
### Weekly Refresh Credits
Paid plans include a small daily credit allowance that does not count toward your plan limit. Each day, usage up to the daily refresh amount is excluded from billable usage. This allowance resets every 24 hours and does not carry over - use it or lose it.
Paid plans include a weekly credit allowance that does not count toward your plan limit. Each week, usage up to the weekly refresh amount is excluded from billable usage. This allowance resets every 7 days from your billing period start and does not carry over - use it or lose it.
| Plan | Daily Refresh |
|------|---------------|
| **Pro** | 50 credits/day ($0.25) |
| **Max** | 200 credits/day ($1.00) |
| Plan | Weekly Refresh |
|------|----------------|
| **Pro** | 2,000 credits/week ($10.00) |
| **Max** | 4,000 credits/week ($20.00) |
For team plans, the daily refresh scales with seats (e.g. Max for Teams with 3 seats = 600 credits/day).
For team plans, the weekly refresh scales with seats (e.g. Max for Teams with 3 seats = 12,000 credits/week).
### Annual Billing
@@ -573,7 +573,7 @@ import { FAQ } from '@/components/ui/faq'
<FAQ items={[
{ question: "How much does a single workflow run cost?", answer: "Every run incurs a base charge of 1 credit ($0.005). On top of that, any AI model usage is billed based on token consumption. Workflows that do not use AI blocks only pay the base run charge." },
{ question: "What is the credit-to-dollar conversion rate?", answer: "1 credit equals $0.005. All plan limits, usage meters, and billing thresholds in the Sim UI are displayed in credits." },
{ question: "Do unused daily refresh credits carry over?", answer: "No. Daily refresh credits reset every 24 hours and do not accumulate. If you do not use them within the day, they are lost." },
{ question: "Do unused weekly refresh credits carry over?", answer: "No. Weekly refresh credits reset every 7 days and do not accumulate. If you do not use them within the week, they are lost." },
{ question: "What happens when I exceed my plan's credit limit?", answer: "By default, your usage is capped at your plan's included credits and runs will stop. If you enable on-demand billing or manually raise your usage limit in Settings, you can continue running workflows and pay for the overage at the end of the billing period." },
{ question: "How does the 1.1x hosted model multiplier work?", answer: "When you use Sim's hosted API keys (instead of bringing your own), a 1.1x multiplier is applied to the base model pricing for Agent blocks. This covers infrastructure and API management costs. You can avoid this multiplier by using your own API keys via the BYOK feature." },
{ question: "Are there any free options for AI models?", answer: "Yes. If you run local models through Ollama or VLLM, there are no API costs for those model calls. You still pay the base run charge of 1 credit per run." },
@@ -186,17 +186,11 @@ Once enabled, retention settings are configurable through **Settings → Enterpr
### PII redaction
PII redaction runs against a standalone [Presidio](https://microsoft.github.io/presidio/) service. Deploy it (see `apps/pii`) and point Sim at it, then enable the redaction surfaces:
PII redaction runs against a standalone [Presidio](https://microsoft.github.io/presidio/) service. Deploy it (see `apps/pii`) and point Sim at it:
```bash
# The Presidio service exposing /analyze and /anonymize
PII_URL=http://localhost:5001
# Expose the log-redaction stage and the Data Retention PII section
PII_REDACTION=true
# Additionally expose the execution-altering stages (Workflow input, Block outputs)
PII_GRANULAR_REDACTION=true
```
`PII_GRANULAR_REDACTION` layers on top of `PII_REDACTION` — with only `PII_REDACTION` enabled, just the **Logs** stage is configurable.
All PII stages are configurable under **Settings → Enterprise → Data Retention**.
@@ -12,7 +12,7 @@ On Sim Cloud, enterprise features are unlocked by an Enterprise subscription. Se
There are two parts to getting this right, and skipping the second is the most common reason features appear to do nothing:
1. **Enable the features** with `ENTERPRISE_ENABLED`.
2. **Give them an organization to apply to.** Whitelabeling, PII redaction, permission groups, data drains, and audit scoping all read their settings from the organization that owns a workspace. A deployment where everyone works in personal workspaces has no organization for those settings to come from.
2. **Give them an organization to apply to.** Whitelabeling, PII redaction, permission groups, custom blocks, data drains, and audit scoping all read their settings from the organization that owns a workspace. A deployment where everyone works in personal workspaces has no organization for those settings to come from.
## Enable the feature set
@@ -24,7 +24,7 @@ NEXT_PUBLIC_ENTERPRISE_ENABLED=true
```
That turns on organizations, permission groups, SSO, whitelabeling, audit logs,
session policies, data retention, data drains, workspace forks, the Sandbox
custom blocks, session policies, data retention, data drains, workspace forks, the Sandbox
entitlement, and the inbox. Sandboxes remain unavailable until their remote
provider and dedicated Function base are configured.
@@ -49,6 +49,7 @@ The individual flags also work on their own if you would rather opt in one at a
| SAML and OIDC sign-in | `SSO_ENABLED` | `NEXT_PUBLIC_SSO_ENABLED` |
| Custom branding | `WHITELABELING_ENABLED` | `NEXT_PUBLIC_WHITELABELING_ENABLED` |
| Audit logs | `AUDIT_LOGS_ENABLED` | `NEXT_PUBLIC_AUDIT_LOGS_ENABLED` |
| Custom blocks | `CUSTOM_BLOCKS_ENABLED` | `NEXT_PUBLIC_CUSTOM_BLOCKS_ENABLED` |
| Session policies | `SESSION_POLICIES_ENABLED` | `NEXT_PUBLIC_SESSION_POLICIES_ENABLED` |
| Data retention deletion | `DATA_RETENTION_ENABLED` | `NEXT_PUBLIC_DATA_RETENTION_ENABLED` |
| Data drains | `DATA_DRAINS_ENABLED` | `NEXT_PUBLIC_DATA_DRAINS_ENABLED` |
@@ -179,8 +179,6 @@ See [Observability](/platform/self-hosting/observability).
|----------|-------------|
| `COPILOT_API_KEY` | API key for Chat. Without it the Sim Chat block, scheduled prompt jobs, and Inbox cannot run |
| `NEXT_PUBLIC_CHAT_DISABLED` | Set to `true` to hide the Chat module: the workspace lands on your first workflow, with no chats list, scheduled tasks, or editor Chat panel. Chat is shown when unset; `npx sim-setup` sets it for you if you skip the chat key |
| `PII_REDACTION` | Redact PII from workflow logs via Data Retention rules; requires the PII service and a cluster-reachable `INTERNAL_API_BASE_URL` |
| `PII_GRANULAR_REDACTION` | Additionally expose the execution-altering redaction stages |
| `DURABLE_SECRET_PROVENANCE_ENFORCED_SURFACES` | Durable stores where a value whose secret provenance was never recorded fails the run instead of logging a warning. `all`, or a comma-separated subset of `memory`, `table-row`, `knowledge`. Unset (nothing enforced) by default |
| `ADMIN_API_KEY` | Admin API key for GitOps operations and organization provisioning |
@@ -194,7 +194,7 @@ Webhook triggers receive callbacks from the provider and must be able to verify
| Variable | Needed for |
|---|---|
| `SLACK_SIGNING_SECRET` | Verifying Slack event and slash-command signatures |
| `SLACK_EXTENDED_SCOPES` / `NEXT_PUBLIC_SLACK_EXTENDED_SCOPES` | Requesting the broader Slack scope set |
| `SLACK_EXTENDED_SCOPES` / `NEXT_PUBLIC_SLACK_EXTENDED_SCOPES` | Enabling the native Sim-app trigger and its broader Slack scope set; set both to the same value |
Your deployment must also be reachable from the provider's servers for webhook triggers to fire — a Sim instance on a private network can use polling triggers but not webhook triggers. Polling triggers additionally require the scheduler; see [Background Jobs](/platform/self-hosting/background-jobs).
@@ -74,7 +74,6 @@ pii:
enabled: true
app:
env:
PII_REDACTION: "true"
INTERNAL_API_BASE_URL: "http://sim-app.simstudio.svc.cluster.local:3000"
```
@@ -164,7 +164,6 @@ pii:
app:
env:
PII_REDACTION: "true"
INTERNAL_API_BASE_URL: "http://sim-app.simstudio.svc.cluster.local:3000"
```
@@ -119,7 +119,7 @@ Live tool-call chips stream for **OpenAI, Anthropic, Azure Anthropic, Google, Ve
| Groq | Full thinking deltas | `groq/openai/gpt-oss-120b`, `groq/openai/gpt-oss-20b`, `groq/openai/gpt-oss-safeguard-20b`, `groq/qwen/qwen3.6-27b` |
| Meta | Not streamed | `muse-spark-1.1` |
| Kimi | Full thinking deltas | `kimi-k2.6` |
| Z.ai | Full thinking deltas | `glm-5.2`, `glm-5.1`, `glm-5`, `glm-5-turbo`, `glm-4.7`, `glm-4.6`, `glm-4.5`, `glm-4.5-air` |
| Z.ai | Full thinking deltas | `glm-5.3`, `glm-5.2`, `glm-5.1`, `glm-5`, `glm-5-turbo`, `glm-4.7`, `glm-4.6`, `glm-4.5`, `glm-4.5-air` |
{/* agent-stream-capabilities:end */}
@@ -174,7 +174,7 @@ Access resume data in downstream blocks using `<blockId.fieldName>`.
## API Execute Behavior
When triggering a workflow through `POST /api/v2/workflows/{id}/execute`, HITL blocks cause the execution to pause and return the `_resume` data in the v2 response envelope. The legacy `POST /api/workflows/{id}/execute` endpoint remains available for existing integrations.
When triggering a workflow through `POST /api/v2/workflows/{workflowId}/execute`, HITL blocks cause the execution to pause and return the `_resume` data in the v2 response envelope. The legacy `POST /api/workflows/{id}/execute` endpoint remains available for existing integrations.
<Tabs items={['Sync (JSON)', 'Stream (SSE)', 'Async']}>
<Tab>
@@ -22,6 +22,8 @@
"(generated)/credentials",
"(generated)/secrets",
"(generated)/billing",
"(generated)/catalog",
"(generated)/meta",
"(generated)/audit-logs"
]
}
@@ -22,6 +22,8 @@
"(generated)/credentials",
"(generated)/secrets",
"(generated)/billing",
"(generated)/catalog",
"(generated)/meta",
"(generated)/audit-logs"
]
}
@@ -22,6 +22,8 @@
"(generated)/credentials",
"(generated)/secrets",
"(generated)/billing",
"(generated)/catalog",
"(generated)/meta",
"(generated)/audit-logs"
]
}
@@ -22,6 +22,8 @@
"(generated)/credentials",
"(generated)/secrets",
"(generated)/billing",
"(generated)/catalog",
"(generated)/meta",
"(generated)/audit-logs"
]
}
+39
View File
@@ -0,0 +1,39 @@
import { readFileSync } from 'node:fs'
import path from 'node:path'
import { loader, multiple } from 'fumadocs-core/source'
import { describe, expect, it } from 'vitest'
import { i18n } from '@/lib/i18n'
import { createApiReferenceSource } from '@/lib/openapi-source'
interface ApiReferenceMeta {
pages: string[]
}
describe('OpenAPI source', () => {
it('resolves every generated navigation group for every locale', async () => {
const source = loader(multiple({ openapi: await createApiReferenceSource() }), {
baseUrl: '/',
i18n,
})
for (const locale of i18n.languages) {
const metaPath = path.resolve(
import.meta.dirname,
`../content/docs/${locale}/api-reference/meta.json`
)
const meta = JSON.parse(readFileSync(metaPath, 'utf8')) as ApiReferenceMeta
const generatedGroups = meta.pages.filter((page) => page.startsWith('(generated)/'))
expect(generatedGroups).toContain('(generated)/catalog')
expect(generatedGroups).toContain('(generated)/meta')
const pages = source.getPages(locale)
for (const group of generatedGroups) {
const groupSlug = group.replace('(generated)/', '')
const localePrefix = locale === i18n.defaultLanguage ? '' : `/${locale}`
const groupUrlPrefix = `${localePrefix}/api-reference/${groupSlug}/`
expect(pages.some((page) => page.url.startsWith(groupUrlPrefix))).toBe(true)
}
}
})
})
+10
View File
@@ -0,0 +1,10 @@
import { openapiSource } from 'fumadocs-openapi/server'
import { openapi } from '@/lib/openapi'
/** Generates the virtual API-reference pages consumed by the docs source loader. */
export function createApiReferenceSource() {
return openapiSource(openapi, {
baseDir: 'en/api-reference/(generated)',
groupBy: 'tag',
})
}
+23
View File
@@ -189,6 +189,29 @@ export const DOCS_REDIRECTS: DocsRedirect[] = [
* must resolve.
*/
// Pure operationId renames — same path and method, v1 -> v2.
{
// `/rows/find` became `/rows/search`: same operation, renamed once the
// surface settled on `query` for a structured predicate and `search` for
// text.
source: '/api-reference/tables/findTableRows',
destination: '/api-reference/tables/searchTableRows',
permanent: true,
},
{
// `/columns/run` became `POST /tables/{tableId}/dispatches`: it always
// created a dispatch and was polled as one, and `GET .../dispatches`
// already sat at the path it now posts to.
//
// These two are the only operations that pass retired a *published* slug —
// confirmed by diffing operationIds in the committed specs, not by reading
// the diff, because a path can move while its operationId (and therefore
// its docs slug) stays put, and an operationId can change without the path
// moving. Everything else renamed alongside them was added and removed
// within the same unreleased branch.
source: '/api-reference/tables/runTableColumns',
destination: '/api-reference/tables/createTableDispatch',
permanent: true,
},
{
source: '/api-reference/audit-logs/getAuditLogDetails',
destination: '/api-reference/audit-logs/getAuditLog',
+2 -6
View File
@@ -1,10 +1,9 @@
import { createElement, Fragment } from 'react'
import { loader, multiple } from 'fumadocs-core/source'
import type { DocData, DocMethods } from 'fumadocs-mdx/runtime/types'
import { openapiSource } from 'fumadocs-openapi/server'
import { docs } from '@/.source/server'
import { i18n } from './i18n'
import { openapi } from './openapi'
import { createApiReferenceSource } from './openapi-source'
const METHOD_COLORS: Record<string, string> = {
GET: 'text-green-600 dark:text-green-400',
@@ -80,10 +79,7 @@ function openapiPluginBadgeLeft() {
export const source = loader(
multiple({
docs: docs.toFumadocsSource(),
openapi: await openapiSource(openapi, {
baseDir: 'en/api-reference/(generated)',
groupBy: 'tag',
}),
openapi: await createApiReferenceSource(),
}),
{
baseUrl: '/',
+13 -7
View File
@@ -43,9 +43,9 @@
"name": "workspaceId",
"in": "query",
"required": false,
"description": "Workspace whose payer should be resolved. Workspace API keys are pinned to their own workspace.",
"description": "Workspace whose payer should be resolved. A workspace API key is pinned to its own workspace: any other id answers `404 Workspace not found`, which is also what an id that does not exist answers.",
"schema": {
"description": "Workspace whose payer should be resolved. Workspace API keys are pinned to their own workspace.",
"description": "Workspace whose payer should be resolved. A workspace API key is pinned to its own workspace: any other id answers `404 Workspace not found`, which is also what an id that does not exist answers.",
"type": "string",
"minLength": 1,
"maxLength": 128
@@ -130,9 +130,9 @@
"name": "workspaceId",
"in": "query",
"required": false,
"description": "Restrict results to one workspace whose payer the caller can inspect.",
"description": "Narrow the ledger to usage events attributed to one workspace. It does not change whose events are reported — a personal API key always reports the usage of the person holding it, and a workspace API key always reports its own workspace's complete ledger across every member. The response `scope` field says which of the two you received. A workspace API key is pinned to its own workspace: any other id answers `404 Workspace not found`, which is also what an id that does not exist answers.",
"schema": {
"description": "Restrict results to one workspace whose payer the caller can inspect.",
"description": "Narrow the ledger to usage events attributed to one workspace. It does not change whose events are reported — a personal API key always reports the usage of the person holding it, and a workspace API key always reports its own workspace's complete ledger across every member. The response `scope` field says which of the two you received. A workspace API key is pinned to its own workspace: any other id answers `404 Workspace not found`, which is also what an id that does not exist answers.",
"type": "string",
"minLength": 1,
"maxLength": 128
@@ -452,7 +452,7 @@
"description": "Human-readable explanation of the error."
},
"details": {
"description": "Structured error details. On a `403` whose cause a caller can act on, this carries a `code` from a closed set:\n- `INSUFFICIENT_WORKSPACE_ROLE` — The caller has access to the workspace but its role is below the one this operation requires.\n- `PERSONAL_API_KEYS_DISABLED` — The workspace's organization does not allow personal API keys. Use a workspace API key.\n- `WORKSPACE_KEY_OPERATION_NOT_PERMITTED` — This operation is not available to a workspace-scoped API key. Use a personal API key.\n- `PRINCIPAL_KIND_NOT_PERMITTED` — This operation does not accept the callers kind of API key.\n- `ORGANIZATION_MEMBERSHIP_REQUIRED` — The caller is not a member of the organization it named.\n- `ORGANIZATION_ADMIN_REQUIRED` — The caller is a member of the organization but not an admin or owner.\n- `ENTERPRISE_PLAN_REQUIRED` — The organization has no active enterprise subscription.\n- `ORGANIZATION_PLAN_REQUIRED` — The organization has no active organization subscription (Pro for Teams, Max for Teams, or Enterprise).\n- `AUDIT_LOGS_DISABLED` — Audit logging is not enabled for this deployment.\n- `SKILL_EDITOR_ACCESS_REQUIRED` — The caller can write in the workspace but is not an editor of this skill.\n- `SECRET_ADMIN_ACCESS_REQUIRED` — The caller can write in the workspace but is not an admin of this secret. Ask a workspace admin, or someone holding admin on the secret, to grant access or set the value.\n- `WORKSPACE_RESOURCE_LIMIT_REACHED` — The workspace already holds the maximum number of resources of this kind. Delete one, or contact Sim to raise the limit; the message names the ceiling.\n- `PUBLIC_SHARING_NOT_ALLOWED` — The workspace's organization does not permit sharing this resource publicly. An organization admin controls the policy.\n- `CREDENTIAL_ADMIN_ACCESS_REQUIRED` — The caller can reach the workspace but cannot administer this credential.\n- `MCP_SERVER_URL_NOT_ALLOWED` — The supplied MCP server URL is outside the allowed domains or resolves to an internal address."
"description": "Structured error details. On a `403` whose cause a caller can act on, this carries a `code` from a closed set:\n- `INSUFFICIENT_WORKSPACE_ROLE` — The caller has access to the workspace but its role is below the one this operation requires.\n- `PERSONAL_API_KEYS_DISABLED` — The workspace's organization does not allow personal API keys. Use a workspace API key.\n- `WORKSPACE_KEY_OPERATION_NOT_PERMITTED` — This operation is not available to a workspace-scoped API key. Use a personal API key.\n- `PRINCIPAL_KIND_NOT_PERMITTED` — This operation does not accept the callers kind of API key.\n- `ORGANIZATION_MEMBERSHIP_REQUIRED` — The caller is not a member of the organization it named.\n- `ORGANIZATION_ADMIN_REQUIRED` — The caller is a member of the organization but not an admin or owner.\n- `ENTERPRISE_PLAN_REQUIRED` — The organization has no active enterprise subscription.\n- `ORGANIZATION_PLAN_REQUIRED` — The organization has no active organization subscription (Pro for Teams, Max for Teams, or Enterprise).\n- `AUDIT_LOGS_DISABLED` — Audit logging is not enabled for this deployment.\n- `SKILL_EDITOR_ACCESS_REQUIRED` — The caller can write in the workspace but is not an editor of this skill.\n- `SECRET_ADMIN_ACCESS_REQUIRED` — The caller can write in the workspace but is not an admin of this secret. Ask a workspace admin, or someone holding admin on the secret, to grant access or set the value.\n- `WORKSPACE_RESOURCE_LIMIT_REACHED` — The workspace already holds the maximum number of resources of this kind. Delete one, or contact Sim to raise the limit; the message names the ceiling.\n- `PUBLIC_SHARING_NOT_ALLOWED` — The workspace's organization does not permit sharing this resource publicly. An organization admin controls the policy.\n- `CREDENTIAL_ADMIN_ACCESS_REQUIRED` — The caller can reach the workspace but cannot administer this credential.\n- `MCP_SERVER_URL_NOT_ALLOWED` — The supplied MCP server URL is outside the allowed domains or resolves to an internal address.\n- `WORKSPACE_PLAN_CAPABILITY_REQUIRED` — The workspace's plan does not include a capability this request depends on. The message names the capability; upgrading the workspace's plan is the remedy.\n- `CHAT_AUTH_MODE_NOT_PERMITTED` — The workspace's permission group does not allow the chat authentication mode the request selected. A mode already saved on the deployment may still be re-saved; changing to a disallowed one cannot.\n- `CONNECTOR_MANAGED_RESOURCE_READ_ONLY` — This resource is managed by a knowledge base connector and cannot be edited directly. Change it at the source and re-sync, or exclude the document from the connector."
}
},
"required": ["code", "message"],
@@ -722,9 +722,14 @@
}
],
"description": "Opaque cursor for the next page. Send it back as `cursor`; `null` means there is nothing further to fetch. Never construct one yourself."
},
"scope": {
"type": "string",
"enum": ["user", "workspace"],
"description": "Whose usage this page reports. `user` — the events of the person whose personal API key made the request, narrowed by `workspaceId` when one was given; this omits other members' usage. `workspace` — every member's events for the workspace a workspace API key is pinned to."
}
},
"required": ["data", "nextCursor"],
"required": ["data", "nextCursor", "scope"],
"additionalProperties": false,
"title": "Billing log list response",
"description": "A cursor-paginated page of credit-consuming usage events.",
@@ -741,7 +746,8 @@
"creditCost": 12
}
],
"nextCursor": null
"nextCursor": null,
"scope": "workspace"
}
]
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+548 -25
View File
@@ -2,7 +2,7 @@
"openapi": "3.1.0",
"info": {
"title": "Sim API v2 — Logs",
"description": "Version 2 of the Sim REST API for listing workflow execution logs and retrieving complete diagnostic run snapshots.",
"description": "Version 2 of the Sim REST API for workflow execution logs: listing and sorting runs with filters, retrieving complete diagnostic run snapshots, and reading bucketed execution statistics.",
"version": "2.0.0",
"contact": {
"name": "Sim Support",
@@ -36,7 +36,7 @@
"get": {
"operationId": "listLogs",
"summary": "List Logs",
"description": "List workflow execution logs for a workspace with filters, selectable detail, and opaque cursor pagination. Runs are hard-deleted once they pass the payer's log retention window, so an older run is simply absent rather than reported as removed. The window is 30 days from run start on the free plan, unbounded on Pro and Team, and set per organization on Enterprise with an optional per-workspace override.",
"description": "List workflow execution logs for a workspace with filters, selectable detail, sorting by start time, duration, cost, or status, and opaque cursor pagination. Chat and Sim-agent job runs join the sequence with `includeJobRuns=true`, which is accepted only under `sortBy=startedAt` — their cost is stored as a document and their status is not comparable, so they cannot participate in the other orderings. Each item's `files` lists only the files the run itself produced, addressed by `downloadPath`; input attachments a caller supplied are read through the files API instead. Runs are hard-deleted once they pass the payer's log retention window, so an older run is simply absent rather than reported as removed. The window is 30 days from run start on the free plan, unbounded on Pro and Team, and set per organization on Enterprise with an optional per-workspace override. A workspace folder tree over 10,000 folders is a `413`.",
"tags": ["Logs"],
"parameters": [
{
@@ -55,20 +55,20 @@
"name": "workflowIds",
"in": "query",
"required": false,
"description": "Comma-separated workflow identifiers to include. An empty entry is rejected.",
"description": "Comma-separated workflow identifiers to include. An empty entry is rejected. At most 200 entries.",
"schema": {
"type": "string",
"description": "Comma-separated workflow identifiers to include. An empty entry is rejected."
"description": "Comma-separated workflow identifiers to include. An empty entry is rejected. At most 200 entries."
}
},
{
"name": "triggers",
"in": "query",
"required": false,
"description": "Comma-separated trigger types to include. An empty entry is rejected. Values are matched exactly and are case-sensitive — every recorded trigger is lowercase, so `API` matches nothing while `api` matches. The vocabulary is open: it covers the core trigger types (`manual`, `api`, `schedule`, `chat`, `webhook`, `mcp`, `copilot`, `workflow`, `custom_block`) and the provider id of any webhook trigger (`slack`, `gmail`, `github`, …), so an unrecognized member is not rejected — it selects no runs. The literal value `all` is a sentinel that disables this filter entirely, so a list containing it returns runs of every trigger type; no real trigger type is named `all`.",
"description": "Comma-separated trigger types to include. An empty entry is rejected. Values are matched exactly and are case-sensitive — every recorded trigger is lowercase, so `API` matches nothing while `api` matches. The vocabulary is open: it covers the core trigger types (`manual`, `api`, `schedule`, `chat`, `webhook`, `mcp`, `copilot`, `workflow`, `custom_block`) and the provider id of any webhook trigger (`slack`, `gmail`, `github`, …), so an unrecognized member is not rejected — it selects no runs. The literal value `all` is a sentinel that disables this filter entirely, so a list containing it returns runs of every trigger type; no real trigger type is named `all`. At most 100 entries.",
"schema": {
"type": "string",
"description": "Comma-separated trigger types to include. An empty entry is rejected. Values are matched exactly and are case-sensitive — every recorded trigger is lowercase, so `API` matches nothing while `api` matches. The vocabulary is open: it covers the core trigger types (`manual`, `api`, `schedule`, `chat`, `webhook`, `mcp`, `copilot`, `workflow`, `custom_block`) and the provider id of any webhook trigger (`slack`, `gmail`, `github`, …), so an unrecognized member is not rejected — it selects no runs. The literal value `all` is a sentinel that disables this filter entirely, so a list containing it returns runs of every trigger type; no real trigger type is named `all`."
"description": "Comma-separated trigger types to include. An empty entry is rejected. Values are matched exactly and are case-sensitive — every recorded trigger is lowercase, so `API` matches nothing while `api` matches. The vocabulary is open: it covers the core trigger types (`manual`, `api`, `schedule`, `chat`, `webhook`, `mcp`, `copilot`, `workflow`, `custom_block`) and the provider id of any webhook trigger (`slack`, `gmail`, `github`, …), so an unrecognized member is not rejected — it selects no runs. The literal value `all` is a sentinel that disables this filter entirely, so a list containing it returns runs of every trigger type; no real trigger type is named `all`. At most 100 entries."
}
},
{
@@ -168,12 +168,12 @@
"name": "details",
"in": "query",
"required": false,
"description": "Response detail level. `full` adds the `workflow` summary to every item. `includeTraceSpans=true` and `includeFinalOutput=true` each imply `full`, so either one adds `workflow` even when `details=basic` is sent explicitly.",
"description": "Response detail level. `full` adds the `workflow` summary to every workflow run; a job run never carries one, whatever this is set to. `includeTraceSpans=true` and `includeFinalOutput=true` each imply `full`, so either one adds `workflow` even when `details=basic` is sent explicitly.",
"schema": {
"default": "basic",
"type": "string",
"enum": ["basic", "full"],
"description": "Response detail level. `full` adds the `workflow` summary to every item. `includeTraceSpans=true` and `includeFinalOutput=true` each imply `full`, so either one adds `workflow` even when `details=basic` is sent explicitly."
"description": "Response detail level. `full` adds the `workflow` summary to every workflow run; a job run never carries one, whatever this is set to. `includeTraceSpans=true` and `includeFinalOutput=true` each imply `full`, so either one adds `workflow` even when `details=basic` is sent explicitly."
}
},
{
@@ -219,15 +219,35 @@
}
},
{
"name": "order",
"name": "status",
"in": "query",
"required": false,
"description": "Sort direction by execution start time. This list is sortable only by execution start time, so it takes `order` in place of `sortBy`/`sortOrder`, which it rejects.",
"description": "Comma-separated execution statuses to include, from `pending` | `running` | `paused` | `redacting` | `completed` | `failed` | `cancelled`. An empty entry is rejected. ANDed with `level`, which reports severity rather than lifecycle.",
"schema": {
"default": "desc",
"description": "Sort direction by execution start time. This list is sortable only by execution start time, so it takes `order` in place of `sortBy`/`sortOrder`, which it rejects.",
"type": "string",
"enum": ["asc", "desc"]
"description": "Comma-separated execution statuses to include, from `pending` | `running` | `paused` | `redacting` | `completed` | `failed` | `cancelled`. An empty entry is rejected. ANDed with `level`, which reports severity rather than lifecycle."
}
},
{
"name": "workflowName",
"in": "query",
"required": false,
"description": "Case-insensitive substring match against the run's workflow name. Runs whose workflow has been deleted match nothing, because the name is no longer joinable.",
"schema": {
"type": "string",
"minLength": 1,
"maxLength": 200,
"description": "Case-insensitive substring match against the run's workflow name. Runs whose workflow has been deleted match nothing, because the name is no longer joinable."
}
},
{
"name": "includeJobRuns",
"in": "query",
"required": false,
"description": "Whether Chat and Sim-agent job runs join the sequence alongside workflow runs. Job runs report `kind: \"job\"`, carry no `workflow` summary, and never carry a cost ledger. They are dropped entirely — not partially matched — whenever a filter they cannot answer is set (`workflowIds`, `workflowName`, `folderPaths`, `model`, or `status`), so a filter never means two different things across the union. Accepted only under `sortBy=startedAt`: job runs record cost as a document and no comparable status, so they cannot participate in the other orderings.",
"schema": {
"description": "Whether Chat and Sim-agent job runs join the sequence alongside workflow runs. Job runs report `kind: \"job\"`, carry no `workflow` summary, and never carry a cost ledger. They are dropped entirely — not partially matched — whenever a filter they cannot answer is set (`workflowIds`, `workflowName`, `folderPaths`, `model`, or `status`), so a filter never means two different things across the union. Accepted only under `sortBy=startedAt`: job runs record cost as a document and no comparable status, so they cannot participate in the other orderings.",
"type": "boolean"
}
},
{
@@ -243,14 +263,38 @@
"description": "Exact run identifier to match."
}
},
{
"name": "sortBy",
"in": "query",
"required": false,
"description": "Field used to sort the result. `durationMs` and `cost` are null until a run settles; those runs order as though the value were below every recorded one, so they trail an ascending page and lead a descending one. Only `startedAt` can order Chat and Sim-agent job runs, so any other value is rejected together with `includeJobRuns=true`.",
"schema": {
"default": "startedAt",
"description": "Field used to sort the result. `durationMs` and `cost` are null until a run settles; those runs order as though the value were below every recorded one, so they trail an ascending page and lead a descending one. Only `startedAt` can order Chat and Sim-agent job runs, so any other value is rejected together with `includeJobRuns=true`.",
"type": "string",
"enum": ["startedAt", "durationMs", "cost", "status"]
}
},
{
"name": "sortOrder",
"in": "query",
"required": false,
"description": "Sort direction.",
"schema": {
"default": "desc",
"description": "Sort direction.",
"type": "string",
"enum": ["asc", "desc"]
}
},
{
"name": "folderPaths",
"in": "query",
"required": false,
"description": "Comma-separated workflow folder paths to include. A path that names no folder narrows the result to nothing, so the response is an empty page rather than an error.",
"description": "Comma-separated workflow folder paths to include. At most 100 entries. A path covers its whole subtree, so `/prod` also selects runs in `/prod/nested`. A path that names no folder narrows the result to nothing, so the response is an empty page rather than an error.",
"schema": {
"type": "string",
"description": "Comma-separated workflow folder paths to include. A path that names no folder narrows the result to nothing, so the response is an empty page rather than an error."
"description": "Comma-separated workflow folder paths to include. At most 100 entries. A path covers its whole subtree, so `/prod` also selects runs in `/prod/nested`. A path that names no folder narrows the result to nothing, so the response is an empty page rather than an error."
}
}
],
@@ -288,6 +332,9 @@
"404": {
"$ref": "#/components/responses/NotFound"
},
"413": {
"$ref": "#/components/responses/PayloadTooLarge"
},
"429": {
"$ref": "#/components/responses/RateLimited"
},
@@ -304,7 +351,7 @@
"get": {
"operationId": "getLog",
"summary": "Get Log",
"description": "Retrieve the diagnostic representation of a run, including its workflow snapshot, trace spans, final output, and cost. Trace spans are pruned on their own retention schedule, so an empty `traceSpans` array does not mean the run recorded none.",
"description": "Retrieve the diagnostic representation of a run, including its workflow snapshot, trace spans, final output, and cost. Trace spans are pruned on their own retention schedule, so an empty `traceSpans` array does not mean the run recorded none. A workspace folder tree over 10,000 folders is a `413`. Runs are hard-deleted once they pass the payer's log retention window, so an older run is simply absent rather than reported as removed. The window is 30 days from run start on the free plan, unbounded on Pro and Team, and set per organization on Enterprise with an optional per-workspace override.",
"tags": ["Logs"],
"parameters": [
{
@@ -355,6 +402,156 @@
"404": {
"$ref": "#/components/responses/NotFound"
},
"413": {
"$ref": "#/components/responses/PayloadTooLarge"
},
"429": {
"$ref": "#/components/responses/RateLimited"
},
"500": {
"$ref": "#/components/responses/InternalError"
},
"503": {
"$ref": "#/components/responses/ServiceUnavailable"
}
}
}
},
"/api/v2/logs/stats": {
"get": {
"operationId": "getLogStats",
"summary": "Get Log Statistics",
"description": "Bucketed run counts, success rate, error count, and mean latency for a workspace and for each of its workflows — the aggregate a caller would otherwise have to page every run to compute. The window spans the oldest matching run through the later of the newest matching run and now, divided into exactly `segmentCount` equal buckets whose width is `max(60000, floor(windowMs / segmentCount))` milliseconds. The one-minute floor is a floor on bucket width, not on the window: when it applies, the series runs past `timeBounds.end` and the trailing buckets are empty rather than the window being compressed. A folder path covers its whole subtree. Per-workflow series are capped and `workflowsTruncated` reports whether the cap applied; the workspace totals are always computed from every workflow. Runs are hard-deleted once they pass the payer's log retention window, so an older run is simply absent rather than reported as removed. The window is 30 days from run start on the free plan, unbounded on Pro and Team, and set per organization on Enterprise with an optional per-workspace override. A workspace folder tree over 10,000 folders is a `413`.",
"tags": ["Logs"],
"parameters": [
{
"name": "workspaceId",
"in": "query",
"required": true,
"description": "Workspace whose execution statistics to summarize.",
"schema": {
"type": "string",
"minLength": 1,
"maxLength": 128,
"description": "Workspace whose execution statistics to summarize."
}
},
{
"name": "workflowIds",
"in": "query",
"required": false,
"description": "Comma-separated workflow identifiers to include. At most 200 entries. An empty entry is rejected.",
"schema": {
"type": "string",
"description": "Comma-separated workflow identifiers to include. At most 200 entries. An empty entry is rejected."
}
},
{
"name": "folderPaths",
"in": "query",
"required": false,
"description": "Comma-separated workflow folder paths to include. At most 100 entries. A path covers its whole subtree. A path that names no folder narrows the result to nothing, so the response is an empty page rather than an error.",
"schema": {
"type": "string",
"description": "Comma-separated workflow folder paths to include. At most 100 entries. A path covers its whole subtree. A path that names no folder narrows the result to nothing, so the response is an empty page rather than an error."
}
},
{
"name": "triggers",
"in": "query",
"required": false,
"description": "Comma-separated trigger types to include. An empty entry is rejected. The vocabulary is open, so an unrecognized member selects no runs; the literal `all` disables this filter.",
"schema": {
"type": "string",
"description": "Comma-separated trigger types to include. An empty entry is rejected. The vocabulary is open, so an unrecognized member selects no runs; the literal `all` disables this filter."
}
},
{
"name": "level",
"in": "query",
"required": false,
"description": "Severity level to include.",
"schema": {
"type": "string",
"enum": ["info", "error"],
"description": "Severity level to include."
}
},
{
"name": "startDate",
"in": "query",
"required": false,
"description": "Only include runs started at or after this UTC ISO 8601 timestamp, e.g. `2026-08-06T00:00:00Z`. A date without a time, or a timestamp carrying a UTC offset instead of `Z`, is rejected, as is year `0000`, which names no storable instant.",
"schema": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"description": "Only include runs started at or after this UTC ISO 8601 timestamp, e.g. `2026-08-06T00:00:00Z`. A date without a time, or a timestamp carrying a UTC offset instead of `Z`, is rejected, as is year `0000`, which names no storable instant."
}
},
{
"name": "endDate",
"in": "query",
"required": false,
"description": "Only include runs started at or before this UTC ISO 8601 timestamp, e.g. `2026-08-06T00:00:00Z`. A date without a time, or a timestamp carrying a UTC offset instead of `Z`, is rejected, as is year `0000`, which names no storable instant.",
"schema": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"description": "Only include runs started at or before this UTC ISO 8601 timestamp, e.g. `2026-08-06T00:00:00Z`. A date without a time, or a timestamp carrying a UTC offset instead of `Z`, is rejected, as is year `0000`, which names no storable instant."
}
},
{
"name": "segmentCount",
"in": "query",
"required": false,
"description": "Number of equal time buckets to divide the window into, from 1 to 500. Exactly this many buckets are always returned. Buckets are never narrower than one minute, so on a short window the series extends past the end of the window rather than being compressed, and the trailing buckets are empty.",
"schema": {
"default": 72,
"description": "Number of equal time buckets to divide the window into, from 1 to 500. Exactly this many buckets are always returned. Buckets are never narrower than one minute, so on a short window the series extends past the end of the window rather than being compressed, and the trailing buckets are empty.",
"type": "integer",
"minimum": 1,
"maximum": 500
}
}
],
"responses": {
"200": {
"description": "Bucketed execution statistics for the workspace.",
"headers": {
"X-RateLimit-Limit": {
"$ref": "#/components/headers/X-RateLimit-Limit"
},
"X-RateLimit-Remaining": {
"$ref": "#/components/headers/X-RateLimit-Remaining"
},
"X-RateLimit-Reset": {
"$ref": "#/components/headers/X-RateLimit-Reset"
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/V2LogStatsResponse"
}
}
}
},
"400": {
"$ref": "#/components/responses/BadRequest"
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"403": {
"$ref": "#/components/responses/Forbidden"
},
"404": {
"$ref": "#/components/responses/NotFound"
},
"413": {
"$ref": "#/components/responses/PayloadTooLarge"
},
"429": {
"$ref": "#/components/responses/RateLimited"
},
@@ -496,6 +693,22 @@
}
}
},
"PayloadTooLarge": {
"description": "The request, or a resource collection it must materialize, exceeds the allowed size: an oversized request body, a generated artifact past the download ceiling, or a workspace folder tree too large to load in full.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/V2Error"
},
"example": {
"error": {
"code": "PAYLOAD_TOO_LARGE",
"message": "Request body is too large"
}
}
}
}
},
"RateLimited": {
"description": "The caller exceeded the request rate limit.",
"headers": {
@@ -574,7 +787,7 @@
"description": "Human-readable explanation of the error."
},
"details": {
"description": "Structured error details. On a `403` whose cause a caller can act on, this carries a `code` from a closed set:\n- `INSUFFICIENT_WORKSPACE_ROLE` — The caller has access to the workspace but its role is below the one this operation requires.\n- `PERSONAL_API_KEYS_DISABLED` — The workspace's organization does not allow personal API keys. Use a workspace API key.\n- `WORKSPACE_KEY_OPERATION_NOT_PERMITTED` — This operation is not available to a workspace-scoped API key. Use a personal API key.\n- `PRINCIPAL_KIND_NOT_PERMITTED` — This operation does not accept the callers kind of API key.\n- `ORGANIZATION_MEMBERSHIP_REQUIRED` — The caller is not a member of the organization it named.\n- `ORGANIZATION_ADMIN_REQUIRED` — The caller is a member of the organization but not an admin or owner.\n- `ENTERPRISE_PLAN_REQUIRED` — The organization has no active enterprise subscription.\n- `ORGANIZATION_PLAN_REQUIRED` — The organization has no active organization subscription (Pro for Teams, Max for Teams, or Enterprise).\n- `AUDIT_LOGS_DISABLED` — Audit logging is not enabled for this deployment.\n- `SKILL_EDITOR_ACCESS_REQUIRED` — The caller can write in the workspace but is not an editor of this skill.\n- `SECRET_ADMIN_ACCESS_REQUIRED` — The caller can write in the workspace but is not an admin of this secret. Ask a workspace admin, or someone holding admin on the secret, to grant access or set the value.\n- `WORKSPACE_RESOURCE_LIMIT_REACHED` — The workspace already holds the maximum number of resources of this kind. Delete one, or contact Sim to raise the limit; the message names the ceiling.\n- `PUBLIC_SHARING_NOT_ALLOWED` — The workspace's organization does not permit sharing this resource publicly. An organization admin controls the policy.\n- `CREDENTIAL_ADMIN_ACCESS_REQUIRED` — The caller can reach the workspace but cannot administer this credential.\n- `MCP_SERVER_URL_NOT_ALLOWED` — The supplied MCP server URL is outside the allowed domains or resolves to an internal address."
"description": "Structured error details. On a `403` whose cause a caller can act on, this carries a `code` from a closed set:\n- `INSUFFICIENT_WORKSPACE_ROLE` — The caller has access to the workspace but its role is below the one this operation requires.\n- `PERSONAL_API_KEYS_DISABLED` — The workspace's organization does not allow personal API keys. Use a workspace API key.\n- `WORKSPACE_KEY_OPERATION_NOT_PERMITTED` — This operation is not available to a workspace-scoped API key. Use a personal API key.\n- `PRINCIPAL_KIND_NOT_PERMITTED` — This operation does not accept the callers kind of API key.\n- `ORGANIZATION_MEMBERSHIP_REQUIRED` — The caller is not a member of the organization it named.\n- `ORGANIZATION_ADMIN_REQUIRED` — The caller is a member of the organization but not an admin or owner.\n- `ENTERPRISE_PLAN_REQUIRED` — The organization has no active enterprise subscription.\n- `ORGANIZATION_PLAN_REQUIRED` — The organization has no active organization subscription (Pro for Teams, Max for Teams, or Enterprise).\n- `AUDIT_LOGS_DISABLED` — Audit logging is not enabled for this deployment.\n- `SKILL_EDITOR_ACCESS_REQUIRED` — The caller can write in the workspace but is not an editor of this skill.\n- `SECRET_ADMIN_ACCESS_REQUIRED` — The caller can write in the workspace but is not an admin of this secret. Ask a workspace admin, or someone holding admin on the secret, to grant access or set the value.\n- `WORKSPACE_RESOURCE_LIMIT_REACHED` — The workspace already holds the maximum number of resources of this kind. Delete one, or contact Sim to raise the limit; the message names the ceiling.\n- `PUBLIC_SHARING_NOT_ALLOWED` — The workspace's organization does not permit sharing this resource publicly. An organization admin controls the policy.\n- `CREDENTIAL_ADMIN_ACCESS_REQUIRED` — The caller can reach the workspace but cannot administer this credential.\n- `MCP_SERVER_URL_NOT_ALLOWED` — The supplied MCP server URL is outside the allowed domains or resolves to an internal address.\n- `WORKSPACE_PLAN_CAPABILITY_REQUIRED` — The workspace's plan does not include a capability this request depends on. The message names the capability; upgrading the workspace's plan is the remedy.\n- `CHAT_AUTH_MODE_NOT_PERMITTED` — The workspace's permission group does not allow the chat authentication mode the request selected. A mode already saved on the deployment may still be re-saved; changing to a disallowed one cannot.\n- `CONNECTOR_MANAGED_RESOURCE_READ_ONLY` — This resource is managed by a knowledge base connector and cannot be edited directly. Change it at the source and re-sync, or exclude the document from the connector."
}
},
"required": ["code", "message"],
@@ -598,6 +811,11 @@
"V2LogListItem": {
"type": "object",
"properties": {
"kind": {
"type": "string",
"enum": ["workflow", "job"],
"description": "Whether the run executed a workflow or a Chat / Sim-agent job. Job runs appear only when `includeJobRuns=true`."
},
"runId": {
"type": "string",
"description": "Unique run identifier."
@@ -692,21 +910,21 @@
"type": "null"
}
],
"description": "Cost charged for the run, or null when unavailable."
"description": "Cost charged for the run, or null when the run has neither a recorded total nor an itemized ledger."
},
"files": {
"anyOf": [
{
"type": "array",
"items": {
"description": "Attachment metadata captured for the execution."
"$ref": "#/components/schemas/V2LogFile"
}
},
{
"type": "null"
}
],
"description": "Files attached to the run, or null when none are recorded."
"description": "Files the run produced, or null when none are recorded. Only the run's own output files appear; input attachments a caller supplied are addressed through the files API instead."
},
"workflow": {
"type": "object",
@@ -758,6 +976,7 @@
}
},
"required": [
"kind",
"runId",
"workflowId",
"deploymentVersionId",
@@ -774,6 +993,37 @@
"title": "Execution log summary",
"description": "Summary information for one workflow execution log."
},
"V2LogFile": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Identifier to address this file by on the download endpoint."
},
"name": {
"type": "string",
"description": "File name, including its extension."
},
"size": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991,
"description": "File size in bytes."
},
"type": {
"type": "string",
"description": "MIME type recorded for the file."
},
"downloadPath": {
"type": "string",
"description": "Path to fetch this file's bytes from, relative to the API host."
}
},
"required": ["id", "name", "size", "type", "downloadPath"],
"additionalProperties": false,
"title": "Execution log file",
"description": "A file produced by the run this log records."
},
"LogTraceSpan": {
"title": "Log trace span",
"description": "One recursive operation span in a workflow execution trace.",
@@ -972,6 +1222,7 @@
{
"data": [
{
"kind": "workflow",
"runId": "e4f8d2b6-9a1c-4e3d-8b7f-5c0a2d9e6f13",
"workflowId": "3b1f7c92-8d4e-4a6b-9c0d-5e2f8a714b36",
"deploymentVersionId": "dep_2c4e6a8b0d1f",
@@ -984,7 +1235,15 @@
"cost": {
"total": 0.0032
},
"files": null
"files": [
{
"id": "f1c3a7d0-4b52-4a8e-9f61-2d7c8b3e5a04",
"name": "summary.pdf",
"size": 18422,
"type": "application/pdf",
"downloadPath": "/api/v2/workflows/3b1f7c92-8d4e-4a6b-9c0d-5e2f8a714b36/runs/e4f8d2b6-9a1c-4e3d-8b7f-5c0a2d9e6f13/files/f1c3a7d0-4b52-4a8e-9f61-2d7c8b3e5a04"
}
]
}
],
"nextCursor": "eyJzdGFydGVkQXQiOiIyMDI2LTAxLTE1VDEwOjMwOjAwMFoifQ=="
@@ -1076,14 +1335,14 @@
{
"type": "array",
"items": {
"description": "Attachment metadata captured for the execution."
"$ref": "#/components/schemas/V2LogFile"
}
},
{
"type": "null"
}
],
"description": "Files attached to the run, or null when none are recorded."
"description": "Files the run produced, or null when none are recorded. Only the run's own output files appear; input attachments a caller supplied are addressed through the files API instead."
},
"workflow": {
"type": "object",
@@ -1238,9 +1497,49 @@
"total": {
"type": "number",
"description": "Total execution cost in USD."
},
"items": {
"anyOf": [
{
"type": "array",
"items": {
"type": "object",
"properties": {
"category": {
"type": "string",
"enum": ["fixed", "model", "tool"],
"description": "What the line is for: the run's base fee (`fixed`), one model's inference (`model`), or one metered tool or integration call (`tool`)."
},
"description": {
"type": "string",
"description": "Human-readable name of the billed item, such as the model or tool id."
},
"cost": {
"type": "number",
"description": "Amount billed for this line, in USD."
},
"inputTokens": {
"description": "Input tokens attributed to this line. Absent for lines that do not bill tokens.",
"type": "number"
},
"outputTokens": {
"description": "Output tokens attributed to this line. Absent for lines that do not bill tokens.",
"type": "number"
}
},
"required": ["category", "description", "cost"],
"additionalProperties": false,
"description": "One billed line of a run, folded across every event that billed it."
}
},
{
"type": "null"
}
],
"description": "Billed lines reconciling to `total`, or null when no itemized ledger exists for the run."
}
},
"required": ["total"],
"required": ["total", "items"],
"additionalProperties": false
},
{
@@ -1249,6 +1548,17 @@
],
"description": "Cost charged for the run, or null when unavailable."
},
"workflowInput": {
"anyOf": [
{
"description": "Caller-supplied trigger payload for the run."
},
{
"type": "null"
}
],
"description": "Input the run was triggered with, or null when the run recorded none. Credential-bearing and PII-masked values are redacted the same way `finalOutput` is."
},
"createdAt": {
"type": "string",
"format": "date-time",
@@ -1272,6 +1582,7 @@
"traceSpans",
"finalOutput",
"cost",
"workflowInput",
"createdAt"
],
"additionalProperties": false,
@@ -1323,12 +1634,224 @@
"result": "Hello, world!"
},
"cost": {
"total": 0.0032
"total": 0.0032,
"items": [
{
"category": "fixed",
"description": "Base execution charge",
"cost": 0.001
},
{
"category": "model",
"description": "gpt-5",
"cost": 0.0022,
"inputTokens": 1840,
"outputTokens": 260
}
]
},
"workflowInput": {
"ticketId": "T-4821"
},
"createdAt": "2026-01-15T10:30:00.000Z"
}
}
]
},
"V2WorkflowLogStats": {
"type": "object",
"properties": {
"workflowId": {
"type": "string",
"description": "Workflow identifier, or the literal `deleted` for the single series that collects runs whose workflow no longer exists."
},
"workflowName": {
"type": "string",
"description": "Workflow name, or `Deleted Workflow`."
},
"segments": {
"type": "array",
"items": {
"$ref": "#/components/schemas/V2LogStatsSegment"
},
"description": "One entry per bucket, in order, including buckets with no runs."
},
"totalExecutions": {
"type": "number",
"description": "Runs for this workflow across the window."
},
"totalSuccessful": {
"type": "number",
"description": "Runs for this workflow that did not error."
},
"overallSuccessRate": {
"type": "number",
"description": "Percentage of runs that did not error, from 0 to 100. 100 when there were no runs."
}
},
"required": [
"workflowId",
"workflowName",
"segments",
"totalExecutions",
"totalSuccessful",
"overallSuccessRate"
],
"additionalProperties": false,
"title": "Per-workflow log stats",
"description": "Bucketed run counts and success rate for one workflow."
},
"V2LogStatsSegment": {
"type": "object",
"properties": {
"timestamp": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"description": "ISO 8601 start of the bucket."
},
"totalExecutions": {
"type": "number",
"description": "Runs that started inside the bucket."
},
"successfulExecutions": {
"type": "number",
"description": "Runs in the bucket that did not error."
},
"avgDurationMs": {
"type": "number",
"description": "Mean duration of the bucket's runs in milliseconds, weighted by run count. Zero when no run in the bucket recorded a duration."
}
},
"required": ["timestamp", "totalExecutions", "successfulExecutions", "avgDurationMs"],
"additionalProperties": false,
"title": "Log stats bucket",
"description": "Run counts and mean latency for one time bucket."
},
"V2LogStats": {
"type": "object",
"properties": {
"workflows": {
"type": "array",
"items": {
"$ref": "#/components/schemas/V2WorkflowLogStats"
},
"description": "Per-workflow series, ordered by error rate descending then by name, capped at 200 entries."
},
"workflowsTruncated": {
"type": "boolean",
"description": "Whether `workflows` was cut to 200 entries. The workspace totals and `aggregateSegments` are computed from every workflow before the cut, so they stay exact either way."
},
"aggregateSegments": {
"type": "array",
"items": {
"$ref": "#/components/schemas/V2LogStatsSegment"
},
"description": "Workspace-wide totals per bucket, in the same order as each workflow series."
},
"totalRuns": {
"type": "number",
"description": "Runs in the window across the whole workspace."
},
"totalErrors": {
"type": "number",
"description": "Runs in the window that errored."
},
"avgLatency": {
"type": "number",
"description": "Mean run duration in milliseconds across the window, weighted by run count."
},
"timeBounds": {
"type": "object",
"properties": {
"start": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"description": "ISO 8601 start of the window."
},
"end": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"description": "ISO 8601 end of the window."
}
},
"required": ["start", "end"],
"additionalProperties": false,
"description": "The window the buckets span: the oldest matching run through the later of the newest matching run and now. A workspace with no matching runs reports the trailing 24 hours."
},
"segmentMs": {
"type": "number",
"description": "Width of one bucket in milliseconds."
}
},
"required": [
"workflows",
"workflowsTruncated",
"aggregateSegments",
"totalRuns",
"totalErrors",
"avgLatency",
"timeBounds",
"segmentMs"
],
"additionalProperties": false,
"title": "Execution log statistics",
"description": "Bucketed success rate, error count, and latency for a workspace and each of its workflows."
},
"V2LogStatsResponse": {
"type": "object",
"properties": {
"data": {
"description": "Response data.",
"$ref": "#/components/schemas/V2LogStats"
}
},
"required": ["data"],
"additionalProperties": false,
"title": "Log statistics response",
"description": "Bucketed success rate, error count, and latency for a workspace and its workflows.",
"examples": [
{
"data": {
"workflows": [
{
"workflowId": "3b1f7c92-8d4e-4a6b-9c0d-5e2f8a714b36",
"workflowName": "Customer Support Agent",
"segments": [
{
"timestamp": "2026-01-15T10:00:00.000Z",
"totalExecutions": 40,
"successfulExecutions": 38,
"avgDurationMs": 1180
}
],
"totalExecutions": 40,
"totalSuccessful": 38,
"overallSuccessRate": 95
}
],
"workflowsTruncated": false,
"aggregateSegments": [
{
"timestamp": "2026-01-15T10:00:00.000Z",
"totalExecutions": 40,
"successfulExecutions": 38,
"avgDurationMs": 1180
}
],
"totalRuns": 40,
"totalErrors": 2,
"avgLatency": 1180,
"timeBounds": {
"start": "2026-01-15T10:00:00.000Z",
"end": "2026-01-15T22:00:00.000Z"
},
"segmentMs": 600000
}
}
]
}
}
},
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+10
View File
@@ -0,0 +1,10 @@
import { fileURLToPath } from 'node:url'
import { defineConfig } from 'vitest/config'
export default defineConfig({
resolve: {
alias: {
'@': fileURLToPath(new URL('.', import.meta.url)),
},
},
})
-15
View File
@@ -320,24 +320,9 @@ describe('Socket Server Index Integration', () => {
expect(getWorkflowState).toBeTypeOf('function')
expect(WorkflowOperationSchema).toBeDefined()
})
it.concurrent('should maintain all original functionality after refactoring', async () => {
expect(httpServer).toBeDefined()
expect(io).toBeDefined()
expect(roomManager).toBeDefined()
expect(typeof roomManager.addUserToRoom).toBe('function')
expect(typeof roomManager.removeUserFromRoom).toBe('function')
expect(typeof roomManager.removeSocketFromAllRooms).toBe('function')
expect(typeof roomManager.broadcastPresenceUpdate).toBe('function')
})
})
describe('Error Handling', () => {
it('should have global error handlers configured', () => {
expect(typeof process.on).toBe('function')
})
it('should handle server setup', () => {
expect(httpServer).toBeDefined()
expect(io).toBeDefined()
+3 -2
View File
@@ -182,8 +182,8 @@ CRON_SECRET=your_cron_secret # Use `openssl rand -hex 32` to generate. Authentic
# Usage: curl -H "x-admin-key: your_key" https://your-instance/api/v1/admin/workspaces
# Enterprise Features (Optional - self-hosted). One switch enables organizations, SSO,
# permission groups, audit logs, whitelabeling, session policies, data retention, data
# drains, forks, and the inbox. Set both — the server value grants access, the
# permission groups, audit logs, custom blocks, whitelabeling, session policies, data
# retention, data drains, forks, and the inbox. Set both — the server value grants access, the
# NEXT_PUBLIC_ value decides what the settings UI shows.
# Docs: https://docs.sim.ai/platform/enterprise/self-hosted
# ENTERPRISE_ENABLED=true
@@ -195,6 +195,7 @@ CRON_SECRET=your_cron_secret # Use `openssl rand -hex 32` to generate. Authentic
# SSO_ENABLED= / NEXT_PUBLIC_SSO_ENABLED= # SAML and OIDC sign-in
# WHITELABELING_ENABLED= / NEXT_PUBLIC_WHITELABELING_ENABLED= # Custom branding
# AUDIT_LOGS_ENABLED= / NEXT_PUBLIC_AUDIT_LOGS_ENABLED= # Audit logging
# CUSTOM_BLOCKS_ENABLED= / NEXT_PUBLIC_CUSTOM_BLOCKS_ENABLED= # Reusable org-wide blocks
# SESSION_POLICIES_ENABLED= / NEXT_PUBLIC_SESSION_POLICIES_ENABLED=
# DATA_RETENTION_ENABLED= / NEXT_PUBLIC_DATA_RETENTION_ENABLED= # Runs retention deletion — off by default
# DATA_DRAINS_ENABLED= / NEXT_PUBLIC_DATA_DRAINS_ENABLED= # Export streams
@@ -1,5 +1,6 @@
import { ChipLink } from '@sim/emcn'
import type { Metadata } from 'next'
import { StatusPage } from '@/components/status-page'
export const metadata: Metadata = {
title: 'Page Not Found',
@@ -8,19 +9,13 @@ export const metadata: Metadata = {
export default function BlogAuthorNotFound() {
return (
<main
id='main-content'
className='mx-auto flex min-h-[60vh] w-full max-w-[1460px] flex-col items-center justify-center gap-3 px-20 py-24 text-center max-sm:px-5 max-lg:px-8'
<StatusPage
title='Author not found'
description="The author you're looking for doesn't exist or has been moved."
>
<h1 className='text-balance text-[40px] text-[var(--text-primary)] leading-[110%] tracking-[-0.02em]'>
Author not found
</h1>
<p className='text-[var(--text-muted)] text-lg'>
The author you&apos;re looking for doesn&apos;t exist or has been moved.
</p>
<ChipLink variant='primary' href='/blog' className='mt-3'>
<ChipLink variant='primary' href='/blog'>
Browse blog
</ChipLink>
</main>
</StatusPage>
)
}
+6 -11
View File
@@ -1,5 +1,6 @@
import { ChipLink } from '@sim/emcn'
import type { Metadata } from 'next'
import { StatusPage } from '@/components/status-page'
export const metadata: Metadata = {
title: 'Page Not Found',
@@ -8,19 +9,13 @@ export const metadata: Metadata = {
export default function BlogNotFound() {
return (
<main
id='main-content'
className='mx-auto flex min-h-[60vh] w-full max-w-[1460px] flex-col items-center justify-center gap-3 px-20 py-24 text-center max-sm:px-5 max-lg:px-8'
<StatusPage
title='Post not found'
description="The post you're looking for doesn't exist or has been moved."
>
<h1 className='text-balance text-[40px] text-[var(--text-primary)] leading-[110%] tracking-[-0.02em]'>
Post not found
</h1>
<p className='text-[var(--text-muted)] text-lg'>
The post you&apos;re looking for doesn&apos;t exist or has been moved.
</p>
<ChipLink variant='primary' href='/blog' className='mt-3'>
<ChipLink variant='primary' href='/blog'>
Browse blog
</ChipLink>
</main>
</StatusPage>
)
}
@@ -1,5 +1,6 @@
import { ChipLink } from '@sim/emcn'
import type { Metadata } from 'next'
import { StatusPage } from '@/components/status-page'
export const metadata: Metadata = {
title: 'Page Not Found',
@@ -8,19 +9,13 @@ export const metadata: Metadata = {
export default function ComparisonNotFound() {
return (
<main
id='main-content'
className='mx-auto flex min-h-[60vh] w-full max-w-[1446px] flex-col items-center justify-center gap-3 px-12 py-24 text-center max-sm:px-5 max-lg:px-8'
<StatusPage
title='Comparison not found'
description="The comparison you're looking for doesn't exist or has been moved."
>
<h1 className='text-balance text-[40px] text-[var(--text-primary)] leading-[110%] tracking-[-0.02em]'>
Comparison not found
</h1>
<p className='text-[var(--text-muted)] text-lg'>
The comparison you&apos;re looking for doesn&apos;t exist or has been moved.
</p>
<ChipLink variant='primary' href='/comparisons' className='mt-3'>
<ChipLink variant='primary' href='/comparisons'>
Browse comparisons
</ChipLink>
</main>
</StatusPage>
)
}
@@ -0,0 +1,48 @@
'use client'
import Image from 'next/image'
import {
PREVIEW_SIDEBAR_CHATS,
PREVIEW_SIDEBAR_WORKFLOWS,
} from '@/app/(landing)/components/shared/sidebar-preview-content'
import {
EnterpriseSidebar,
type EnterpriseSidebarProps,
} from '@/app/(landing)/enterprise/components/enterprise-platform-loop/enterprise-sidebar'
interface CapturedPlatformSurfaceProps {
src: string
sizes: string
activeItem: NonNullable<EnterpriseSidebarProps['activeItem']>
}
/**
* A captured product surface with the current landing sidebar rendered over
* the capture's legacy sidebar pixels. Keeping the sidebar live gives every
* landing callout one source of truth while preserving the detailed product
* content in the capture beside it.
*/
export function CapturedPlatformSurface({ src, sizes, activeItem }: CapturedPlatformSurfaceProps) {
return (
<div className='absolute inset-0 overflow-hidden'>
<Image src={src} alt='' fill sizes={sizes} className='object-cover' />
<svg
aria-hidden='true'
className='pointer-events-none absolute inset-0 size-full overflow-hidden'
viewBox='0 0 1280 735'
preserveAspectRatio='xMinYMin meet'
>
<foreignObject width='1280' height='735'>
<div className='flex h-full w-[249px] border-[var(--border)] border-r bg-[var(--surface-1)]'>
<EnterpriseSidebar
chats={PREVIEW_SIDEBAR_CHATS}
workflows={PREVIEW_SIDEBAR_WORKFLOWS}
activeItem={activeItem}
/>
<div className='min-w-0 flex-1 bg-[var(--surface-1)]' />
</div>
</foreignObject>
</svg>
</div>
)
}
@@ -1,14 +1,10 @@
import Image from 'next/image'
import { CalloutFrame } from '@/app/(landing)/components/features/components/callout-frame'
import { CapturedPlatformSurface } from '@/app/(landing)/components/features/components/captured-platform-surface'
/**
* The Integrate beat's callout - the REAL platform Integrations page as one
* floating window: a full capture of the workspace UI (sidebar + Integrations
* tab with the showcase mosaic, search, and Featured sections) taken by
* `exports/readme-banner/capture-integrations-ui.mjs` at the hero shot's card
* geometry (1280x735 @2x), framed by the shared {@link CalloutFrame} so it
* wears the hero platform window's exact chrome (10px radius + layered
* shadow).
* The Integrate beat's callout - the platform Integrations page as one
* floating window. The detailed page capture is paired with the shared live
* landing sidebar so its navigation and footer stay aligned with the product.
*
* The window is oversized (125% of the media stage, ~82% of the capture's
* native scale) and anchored with visually EQUAL top and left insets
@@ -37,12 +33,10 @@ export function IntegrationsCallout() {
className='absolute top-[14.4%] left-[9.6%] w-[125%]'
bodyClassName='aspect-[1280/735]'
>
<Image
<CapturedPlatformSurface
src='/landing/feature-integrate-ui.png'
alt=''
fill
sizes='(max-width: 1023px) 114vw, (max-width: 1460px) 109vw, 1053px'
className='object-cover'
activeItem='Integrations'
/>
</CalloutFrame>
</div>
@@ -1,14 +1,10 @@
import Image from 'next/image'
import { CalloutFrame } from '@/app/(landing)/components/features/components/callout-frame'
import { CapturedPlatformSurface } from '@/app/(landing)/components/features/components/captured-platform-surface'
/**
* The Context beat's callout - the REAL platform Knowledge base page as one
* floating window: a full capture of the workspace UI (sidebar with Knowledge
* base selected + the knowledge list) taken by
* `exports/readme-banner/capture-knowledge-ui.mjs` at the hero shot's card
* geometry (1280x735 @2x), framed by the shared {@link CalloutFrame} so it
* wears the hero platform window's exact chrome (10px radius + layered
* shadow).
* The Context beat's callout - the platform Knowledge bases page as one
* floating window. The detailed page capture is paired with the shared live
* landing sidebar so its navigation and footer stay aligned with the product.
*
* Same oversized treatment as the Integrate card: 125% of the media stage
* with EQUAL top and left insets (96px), so the top-left corner floats free
@@ -22,12 +18,10 @@ export function KnowledgeCallout() {
className='absolute top-[14.4%] left-[9.6%] w-[125%]'
bodyClassName='aspect-[1280/735]'
>
<Image
<CapturedPlatformSurface
src='/landing/feature-context-ui.png'
alt=''
fill
sizes='1050px'
className='object-cover'
activeItem='Knowledge bases'
/>
</CalloutFrame>
</div>
@@ -1,14 +1,10 @@
import Image from 'next/image'
import { CalloutFrame } from '@/app/(landing)/components/features/components/callout-frame'
import { CapturedPlatformSurface } from '@/app/(landing)/components/features/components/captured-platform-surface'
/**
* The Monitor beat's callout - the REAL platform Logs page as one floating
* window: a full capture of the workspace UI (sidebar with Logs selected +
* the seeded run table) taken by
* `exports/readme-banner/capture-logs-ui.mjs` at the hero shot's card
* geometry (1280x735 @2x), framed by the shared {@link CalloutFrame} so it
* wears the hero platform window's exact chrome (10px radius + layered
* shadow).
* The Monitor beat's callout - the platform Logs page as one floating window.
* The detailed page capture is paired with the shared live landing sidebar so
* its navigation and footer stay aligned with the product.
*
* Same oversized treatment as the Context card: 125% of the media stage with
* EQUAL top and left insets (96px), so the top-left corner floats free over
@@ -22,12 +18,10 @@ export function LogsCallout() {
className='absolute top-[14.4%] left-[9.6%] w-[125%]'
bodyClassName='aspect-[1280/735]'
>
<Image
<CapturedPlatformSurface
src='/landing/feature-monitor-ui.png'
alt=''
fill
sizes='1050px'
className='object-cover'
activeItem='Logs'
/>
</CalloutFrame>
</div>
@@ -1,14 +1,18 @@
'use client'
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
import { useEffect, useState } from 'react'
import { cn } from '@sim/emcn'
import {
HeroChatLoop,
type HeroChatPhase,
} from '@/app/(landing)/components/hero/components/hero-chat-loop'
import { HeroWorkflowStage } from '@/app/(landing)/components/hero/components/hero-platform-loop/hero-workflow-stage'
import { SidebarHotspots } from '@/app/(landing)/components/hero/components/hero-platform-loop/sidebar-hotspots'
import { STAGE_BLOCKS } from '@/app/(landing)/components/hero/components/hero-platform-loop/stage-data'
import { HeroLoopShell } from '@/app/(landing)/components/shared/hero-loop-shell'
import {
PREVIEW_SIDEBAR_CHATS,
PREVIEW_SIDEBAR_WORKFLOWS,
} from '@/app/(landing)/components/shared/sidebar-preview-content'
/**
* One pass of the synced loop, matching the REAL platform behavior: the chat
@@ -29,35 +33,14 @@ const TOTAL_MS = 12_500
const RESET_FADE_MS = 260
/**
* The workspace container's interior in the capture's design space (the shot
* is 2560x1470 at 2x, i.e. a 1280x735 CSS layout - oversized vs the 1080x620
* window so the whole UI displays at 84.4%, landing the app's native type at
* cursor.com's ~12.7px demo scale): x 249-1272, y 7-727. The live layer lays
* out at this FIXED size and scales down with the window (`transform: scale`),
* so its text and controls shrink in lockstep with the baked sidebar pixels -
* the "mini app" reading - instead of rendering at 1:1 CSS sizes and looking
* oversized next to the scaled screenshot.
*/
const CHROME_INTERIOR = { width: 1024, height: 721 } as const
/**
* The hero window's live layer - one flex region replaying the REAL Home
* two-pane over the static screenshot. The region covers the workspace
* container's interior (inset a hair inside the shot's baked chrome: the
* horizontal rules 6px from the card top/bottom (the chrome's `p-[8px]` gap is
* tightened to 6px at capture time), the container's left border at ~19.4%,
* and its right border at ~99.4%; the container renders at `6px` radius
* (overridden at capture time from the chrome's 8px so it DISPLAYS at the
* concentric ~4.9px after the 84.4% shot-to-window factor), so the region
* clips itself `rounded-[4px]` to hug the baked corner curves without
* covering them) so every visible outline is the real UI's pixels.
*
* Inside, the layout mirrors `Home`: the {@link HeroChatLoop} is a flex-1
* The homepage's live platform preview, rendered through the same shared shell
* as every product and solutions hero so the sidebar never drifts between
* landing surfaces. Inside, the layout mirrors `Home`: the
* {@link HeroChatLoop} is a flex-1
* `--bg` column; the {@link HeroWorkflowStage} pane animates `w-0 ↔ w-1/2`
* with the real `MothershipView` width transition (200ms,
* `cubic-bezier(0.25,0.1,0.25,1)`, `border-l` only while open) - the baked
* chat|stage divider is covered by this region, so the divider users see is
* the live `border-l`, appearing exactly as it does in the product.
* `cubic-bezier(0.25,0.1,0.25,1)`, `border-l` only while open), matching the
* divider users see in the product.
*
* Both panes stay `pointer-events-none` (decorative, matching the hero's
* `aria-hidden` frame) - blocks are static. Remounting the stage per cycle
@@ -67,28 +50,11 @@ const CHROME_INTERIOR = { width: 1024, height: 721 } as const
* open stage, and fully-built workflow render statically.
*/
export function HeroPlatformLoop() {
const regionRef = useRef<HTMLDivElement>(null)
const [phase, setPhase] = useState<HeroChatPhase>('idle')
const [stageOpen, setStageOpen] = useState(false)
const [builtCount, setBuiltCount] = useState(0)
const [fading, setFading] = useState(false)
const [cycleId, setCycleId] = useState(0)
const [scale, setScale] = useState(1)
// Track the rendered region width and scale the design-space layer to fill
// it, keeping the live layer's proportions locked to the screenshot's.
useLayoutEffect(() => {
const el = regionRef.current
if (!el) return
const measure = () => {
const w = el.getBoundingClientRect().width
if (w > 40) setScale(w / CHROME_INTERIOR.width)
}
measure()
const ro = new ResizeObserver(measure)
ro.observe(el)
return () => ro.disconnect()
}, [])
useEffect(() => {
const media = window.matchMedia('(prefers-reduced-motion: reduce)')
@@ -144,33 +110,27 @@ export function HeroPlatformLoop() {
}, [])
return (
<>
<SidebarHotspots />
<div
ref={regionRef}
className='absolute top-[0.95%] right-[0.55%] bottom-[0.95%] left-[19.45%] overflow-hidden rounded-[4px]'
>
<HeroLoopShell chats={PREVIEW_SIDEBAR_CHATS} workflows={PREVIEW_SIDEBAR_WORKFLOWS}>
<div className='flex h-full w-full overflow-hidden rounded-[6px] border border-[var(--border)] bg-[var(--bg)]'>
<div className='relative h-full min-w-0 flex-1'>
<HeroChatLoop phase={phase} fading={fading} />
</div>
<div
className='flex origin-top-left'
style={{
width: CHROME_INTERIOR.width,
height: CHROME_INTERIOR.height,
transform: `scale(${scale})`,
}}
className={cn(
'h-full shrink-0 overflow-hidden border-[var(--border)] bg-[var(--bg)] transition-[width,min-width,border-width] duration-200 [transition-timing-function:cubic-bezier(0.25,0.1,0.25,1)]',
stageOpen ? 'w-1/2 border-l' : 'w-0 min-w-0 border-l-0'
)}
>
<div className='pointer-events-none relative h-full min-w-0 flex-1'>
<HeroChatLoop phase={phase} fading={fading} />
</div>
<div
className={cn(
'h-full shrink-0 overflow-hidden border-[var(--border)] bg-[var(--bg)] transition-[width,min-width,border-width] duration-200 [transition-timing-function:cubic-bezier(0.25,0.1,0.25,1)]',
stageOpen ? 'w-1/2 border-l' : 'w-0 min-w-0 border-l-0'
'h-full w-full transition-opacity duration-300 ease-out',
fading ? 'opacity-0' : 'opacity-100'
)}
>
<HeroWorkflowStage key={cycleId} builtCount={builtCount} />
</div>
</div>
</div>
</>
</HeroLoopShell>
)
}
@@ -1,9 +1,8 @@
'use client'
import { type CSSProperties, useLayoutEffect, useMemo, useRef, useState } from 'react'
import { useMemo } from 'react'
import { cn } from '@sim/emcn'
import { StageBlockCard } from '@/app/(landing)/components/hero/components/hero-platform-loop/stage-block-card'
import {
blockHeight,
handleAnchors,
STAGE_BLOCKS,
STAGE_CANVAS,
@@ -15,8 +14,6 @@ import {
type BlockDef,
} from '@/app/(landing)/components/hero/components/hero-visual/workflow-data'
/** Upper bound on the canvas render scale (the scale at the full 1300px cap). */
const MAX_STAGE_SCALE = 0.71
/** Breathing room between the canvas bounds and the card edges, in card px. */
const STAGE_MARGIN = 20
@@ -40,11 +37,10 @@ interface HeroWorkflowStageProps {
/**
* The hero window's live workflow canvas - the right-pane counterpart of the
* chat loop. Blocks pop in one by one as `builtCount` advances (staggered
* scale/fade entrances, edges stroke-draw once both endpoints exist) at their
* fixed positions. The edge SVG is `overflow-visible` - SVGs clip
* at their viewport by default, which would cut the lines if a block ever sat
* outside the design-canvas bounds.
* chat loop. One stable SVG viewBox owns both the edge and block coordinate
* systems, so drawing a line or revealing a block never changes the canvas's
* measured scale. Blocks pop in one by one as `builtCount` advances and edges
* stroke-draw once both endpoints exist.
*
* Decorative and `aria-hidden` (via the parent frame), so blocks are NOT
* draggable - `pointer-events-none`, matching the rest of the hero animation.
@@ -64,116 +60,73 @@ export function HeroWorkflowStage({
canvas = STAGE_CANVAS,
selectedId,
}: HeroWorkflowStageProps) {
const containerRef = useRef<HTMLDivElement>(null)
const [scale, setScale] = useState(MAX_STAGE_SCALE)
const blocksById = useMemo(() => new Map(blocks.map((b) => [b.id, b])), [blocks])
// Fit the design canvas to the card: scale down when the pane narrows so the
// branch blocks never clip, capped at the full-width scale. Measures LAYOUT
// size (offsetWidth/Height) - the stage lives inside the platform loop's
// scale-transformed design-space layer, and getBoundingClientRect's visual
// size would compound that outer scale into a double shrink.
useLayoutEffect(() => {
const el = containerRef.current
if (!el) return
const measure = () => {
const w = el.offsetWidth
const h = el.offsetHeight
if (w < 40 || h < 40) return
setScale(
Math.min(
MAX_STAGE_SCALE,
(w - STAGE_MARGIN) / canvas.width,
(h - STAGE_MARGIN) / canvas.height
)
)
}
measure()
const ro = new ResizeObserver(measure)
ro.observe(el)
return () => ro.disconnect()
}, [canvas.width, canvas.height])
const builtIds = useMemo(
() => new Set(blocks.slice(0, builtCount).map((b) => b.id)),
[blocks, builtCount]
)
return (
<div
ref={containerRef}
className='flex h-full w-full items-center justify-center overflow-hidden'
<svg
aria-hidden='true'
className='size-full overflow-hidden'
viewBox={`${-STAGE_MARGIN / 2} ${-STAGE_MARGIN / 2} ${canvas.width + STAGE_MARGIN} ${canvas.height + STAGE_MARGIN}`}
preserveAspectRatio='xMidYMid meet'
fill='none'
>
<div
className='relative shrink-0'
style={{
width: canvas.width * scale,
height: canvas.height * scale,
}}
>
<div
className='absolute top-0 left-0'
style={{
width: canvas.width,
height: canvas.height,
transform: `scale(${scale})`,
transformOrigin: '0 0',
}}
>
<svg
className='pointer-events-none absolute inset-0 overflow-visible'
width={canvas.width}
height={canvas.height}
viewBox={`0 0 ${canvas.width} ${canvas.height}`}
fill='none'
aria-hidden='true'
>
{edges.map(([from, to]) => {
const source = blocksById.get(from)
const target = blocksById.get(to)
if (!source || !target) return null
const visible = builtIds.has(from) && builtIds.has(to)
const s = handleAnchors(source).out
const t = handleAnchors(target).in
return (
<path
key={`${from}-${to}`}
d={verticalSmoothStep(s.x, s.y, t.x, t.y)}
pathLength={1}
stroke='var(--workflow-edge)'
strokeWidth={2}
strokeLinecap='round'
className='transition-[stroke-dashoffset] duration-500 [stroke-dasharray:1] [transition-timing-function:cubic-bezier(0.22,1,0.36,1)]'
style={{ strokeDashoffset: visible ? 0 : 1 } as CSSProperties}
/>
)
})}
</svg>
{edges.map(([from, to]) => {
const source = blocksById.get(from)
const target = blocksById.get(to)
if (!source || !target) return null
const visible = builtIds.has(from) && builtIds.has(to)
const s = handleAnchors(source).out
const t = handleAnchors(target).in
return (
<path
key={`${from}-${to}`}
d={verticalSmoothStep(s.x, s.y, t.x, t.y)}
pathLength={1}
stroke='var(--workflow-edge)'
strokeWidth={2}
strokeLinecap='round'
className={cn(
'transition-[stroke-dashoffset] duration-500 [stroke-dasharray:1] [transition-timing-function:cubic-bezier(0.22,1,0.36,1)]',
visible ? '[stroke-dashoffset:0]' : '[stroke-dashoffset:1]'
)}
/>
)
})}
{blocks.map((block) => {
const built = builtIds.has(block.id)
return (
<div
key={block.id}
{blocks.map((block) => {
const built = builtIds.has(block.id)
return (
<foreignObject
key={block.id}
x={block.x}
y={block.y}
width={BLOCK_WIDTH}
height={blockHeight(block)}
overflow='visible'
>
<div
className={cn(
'pointer-events-none relative size-full origin-center transition-[opacity,scale] duration-300 will-change-[opacity,transform] [transition-timing-function:cubic-bezier(0.22,1,0.36,1)]',
built ? 'scale-100 opacity-100' : 'scale-[0.94] opacity-0'
)}
>
<StageBlockCard block={block} />
<span
aria-hidden
className={cn(
'pointer-events-none absolute transition-[opacity,scale] duration-300 [transition-timing-function:cubic-bezier(0.22,1,0.36,1)]',
built ? 'scale-100 opacity-100' : 'scale-[0.94] opacity-0'
'pointer-events-none absolute inset-0 rounded-[13px] ring-[1.75px] ring-[var(--text-secondary)] transition-opacity duration-300 ease-out',
selectedId === block.id && built ? 'opacity-100' : 'opacity-0'
)}
style={{ left: block.x, top: block.y, width: BLOCK_WIDTH }}
>
<StageBlockCard block={block} />
<span
aria-hidden
className={cn(
'pointer-events-none absolute inset-0 rounded-[13px] ring-[1.75px] ring-[var(--text-secondary)] transition-opacity duration-300 ease-out',
selectedId === block.id && built ? 'opacity-100' : 'opacity-0'
)}
/>
</div>
)
})}
</div>
</div>
</div>
/>
</div>
</foreignObject>
)
})}
</svg>
)
}
@@ -1,89 +1,2 @@
'use client'
import { Tooltip } from '@sim/emcn'
/**
* Cursor-to-bubble gap for tooltips over the mini platform UI - tighter than
* the product-standard 16px so the bubble hugs the cursor proportionately to
* the scaled-down preview.
*/
/** Tight tooltip spacing for the scaled-down platform preview. */
export const HERO_TOOLTIP_OFFSET = 8
/** Percent-of-image bounds shared by both hotspot kinds. */
interface HotspotBounds {
left: string
top: string
width: string
height: string
}
/** An icon control that shows a tooltip on hover, like the real product. */
interface TooltipHotspot extends HotspotBounds {
label: string
}
/** A sidebar nav row that shows the real row-hover highlight on hover. */
interface RowHotspot extends HotspotBounds {
name: string
}
/**
* Icon controls measured from the capture (2560x1470): the collapse-sidebar
* toggle (x428-468, y36-72) and the Workflows section's "More actions"
* ellipsis (x376-412) and "Create workflow" plus (x424-460) at y920-952.
* Copy matches the real product's tooltips.
*/
const TOOLTIP_HOTSPOTS: TooltipHotspot[] = [
{ label: 'Collapse sidebar', left: '16.72%', top: '2.45%', width: '1.56%', height: '2.45%' },
{ label: 'More actions', left: '14.69%', top: '62.59%', width: '1.4%', height: '2.18%' },
{ label: 'Create workflow', left: '16.56%', top: '62.59%', width: '1.4%', height: '2.18%' },
]
/**
* Workspace nav rows (Tables / Files / Knowledge base), boxed like the real
* sidebar row (`h-[30px] rounded-lg`, text rows measured at y598-616, 662-680,
* 726-750): a 60px-tall (at 2x) highlight box centered on each row's text,
* starting left of the row ICON (icons measured at x37-39, the box carries the
* real row's 8px icon gutter) so the icon sits inside the highlight.
*/
const ROW_HOTSPOTS: RowHotspot[] = [
{ name: 'Tables', left: '0.82%', top: '39.25%', width: '17.15%', height: '4.08%' },
{ name: 'Files', left: '0.82%', top: '43.61%', width: '17.15%', height: '4.08%' },
{ name: 'Knowledge base', left: '0.82%', top: '48.16%', width: '17.15%', height: '4.08%' },
]
/**
* Hover layers over the BAKED sidebar pixels. Icon controls (collapse toggle,
* Workflows more/create) get transparent targets wired to the emcn
* {@link Tooltip} with the product's real copy ({@link HERO_TOOLTIP_OFFSET}
* keeps the bubble close over the mini UI). The Workspace nav rows instead
* reproduce the REAL sidebar row-hover state - `--surface-active` on the
* row's rounded box - via `mix-blend-multiply`, which paints over the baked
* white/text pixels to exactly the result of a background behind the text.
*/
export function SidebarHotspots() {
return (
<div aria-hidden='true' className='pointer-events-none absolute inset-0'>
{TOOLTIP_HOTSPOTS.map((spot) => (
<Tooltip.Root key={spot.label}>
<Tooltip.Trigger asChild>
<span
aria-label={spot.label}
className='pointer-events-auto absolute block'
style={{ left: spot.left, top: spot.top, width: spot.width, height: spot.height }}
/>
</Tooltip.Trigger>
<Tooltip.Content offset={HERO_TOOLTIP_OFFSET}>{spot.label}</Tooltip.Content>
</Tooltip.Root>
))}
{ROW_HOTSPOTS.map((row) => (
<span
key={row.name}
aria-label={row.name}
className='pointer-events-auto absolute block cursor-pointer rounded-[6px] mix-blend-multiply transition-colors duration-100 hover:bg-[var(--surface-active)]'
style={{ left: row.left, top: row.top, width: row.width, height: row.height }}
/>
))}
</div>
)
}
@@ -31,23 +31,11 @@ import { TrustedBy } from '@/app/(landing)/components/trusted-by'
* LCP element) behind a white window (a soft three-part shadow stack:
* `0 0 0 1px rgba(0,0,0,0.08)` ring in place of a CSS border, plus
* `0 2px 6px rgba(0,0,0,0.05)` contact and `0 4px 42px rgba(0,0,0,0.06)`
* ambient shadows; no browser toolbar) filled edge to edge by the REAL
* platform UI - a 2x
* screenshot (`hero-platform-ui.png`, 2560x1470: a 1280x735 layout shown in
* the 1080x620 window, so the UI reads at 84.4% - the "mini app" type scale
* cursor.com's demo window uses) of the chat-everywhere two-pane (seeded
* Mothership chat left, staged workflow right) captured from the
* `readme-tour-capture` route via
* `exports/readme-banner/capture-hero-platform.mjs`. The window is
* `rounded-[10px]` - matching cursor.com's demo window - and the shot's
* workspace container renders at the concentric inner radius `4px` (outer
* 10px - 6px gap; overridden at capture time from the chrome's 8px). Only the
* SIDEBAR
* remains visible from the shot: the {@link HeroPlatformLoop} island overlays
* the container interior (full-width chat that stages the workflow pane in,
* replaying the conversation with the goo ThinkingLoader), inset a hair INSIDE
* the shot's own baked outlines so the visible chrome is the real UI's pixels
* - never re-drawn.
* ambient shadows; no browser toolbar) filled edge to edge by the live
* {@link HeroPlatformLoop}. The shared landing sidebar and animated Home
* workspace render together in a fixed 1280x735 design space, so the homepage
* stays aligned with every product and solutions hero without a baked UI
* capture drifting behind it.
* The frame is `1300/720` and the window `1080/620` at `83.08%` width, centered
* - matching cursor.com's hero media proportions, with backdrop showing on all
* four sides. Decorative, `aria-hidden`; the `--surface-3` fill remains as the
@@ -109,14 +97,6 @@ export function Hero() {
/>
<div className='-translate-x-1/2 -translate-y-1/2 absolute top-1/2 left-1/2 flex aspect-[1080/620] w-[83.08%] flex-col overflow-hidden rounded-[10px] bg-[var(--surface-1)] shadow-[0_0_0_1px_rgba(0,0,0,0.08),0_2px_6px_0_rgba(0,0,0,0.05),0_4px_42px_0_rgba(0,0,0,0.06)]'>
<div className='relative flex-1'>
<Image
src='/landing/hero-platform-ui.png'
alt=''
fill
priority
sizes='(max-width: 1460px) 83vw, 1080px'
className='object-cover object-left-top'
/>
<HeroPlatformLoop />
</div>
</div>
@@ -2,22 +2,6 @@ export {
IsoBuildIllustration,
type IsoBuildIllustrationProps,
} from '@/app/(landing)/components/mothership/components/iso-marks/iso-build-illustration'
export {
IsoCubeGrid,
type IsoCubeGridProps,
} from '@/app/(landing)/components/mothership/components/iso-marks/iso-cube-grid'
export {
IsoCubeRow,
type IsoCubeRowProps,
} from '@/app/(landing)/components/mothership/components/iso-marks/iso-cube-row'
export {
IsoFourBox,
type IsoFourBoxProps,
} from '@/app/(landing)/components/mothership/components/iso-marks/iso-four-box'
export {
IsoGridPlane,
type IsoGridPlaneProps,
} from '@/app/(landing)/components/mothership/components/iso-marks/iso-grid-plane'
export {
IsoIngestIllustration,
type IsoIngestIllustrationProps,
@@ -30,11 +14,3 @@ export {
IsoMonitorIllustration,
type IsoMonitorIllustrationProps,
} from '@/app/(landing)/components/mothership/components/iso-marks/iso-monitor-illustration'
export {
IsoStackedPlanes,
type IsoStackedPlanesProps,
} from '@/app/(landing)/components/mothership/components/iso-marks/iso-stacked-planes'
export {
IsoStar,
type IsoStarProps,
} from '@/app/(landing)/components/mothership/components/iso-marks/iso-star'
@@ -1,153 +0,0 @@
'use client'
import { cn } from '@sim/emcn'
import { GooDefs } from '@/app/(landing)/components/mothership/components/iso-marks/goo-defs'
import {
type Edge,
gradientForTone,
isoProject,
type MarkState,
type Pt,
TARGET,
useGooMark,
useMarkIds,
} from '@/app/(landing)/components/mothership/components/iso-marks/use-goo-mark'
/**
* Sim iso goo-mark: CUBE GRID.
* Nine small iso cubes tiled in a 3×3 screen grid. Rest: the nine sit spread
* apart. Hover (gather): they pull in toward the center into a snug grid - still
* clearly nine separate cubes, never merged into a blob. No spin; the motion is
* pure convergence.
*/
interface GridState extends MarkState {
gap: number
tilt: number
tone: number
}
const REST: GridState = { gap: 3.3, tilt: 0.5, tone: 1 }
const HOVER: GridState = { gap: 2.05, tilt: 0.5, tone: 1 }
const GRID = 3
const U = 0.5
const STROKE = 2.4
const GOO_FUSION = 0.55
/** A unit iso cube, projected to screen space and centered on the origin. */
function unitCubeEdges(ky: number): Edge[] {
const corner = (sx: number, sy: number, sz: number) => isoProject(sx * U, sy * U, sz * U, ky)
const c = [
corner(-1, -1, -1),
corner(1, -1, -1),
corner(1, 1, -1),
corner(-1, 1, -1),
corner(-1, -1, 1),
corner(1, -1, 1),
corner(1, 1, 1),
corner(-1, 1, 1),
]
const ed: [number, number][] = [
[0, 1],
[1, 2],
[2, 3],
[3, 0],
[4, 5],
[5, 6],
[6, 7],
[7, 4],
[0, 4],
[1, 5],
[2, 6],
[3, 7],
]
return ed.map(([a, b]) => [c[a], c[b]] as Edge)
}
function buildEdges(s: GridState): Edge[] {
const base = unitCubeEdges(s.tilt)
const edges: Edge[] = []
for (let i = 0; i < GRID; i++) {
for (let j = 0; j < GRID; j++) {
const dx = (i - (GRID - 1) / 2) * s.gap
const dy = (j - (GRID - 1) / 2) * s.gap
for (const [A, B] of base) {
edges.push([
[A[0] + dx, A[1] + dy],
[B[0] + dx, B[1] + dy],
])
}
}
}
return edges
}
function normalizeEdges(edges: Edge[]): Edge[] {
const pts = edges.flat()
let minx = Number.POSITIVE_INFINITY
let maxx = Number.NEGATIVE_INFINITY
let miny = Number.POSITIVE_INFINITY
let maxy = Number.NEGATIVE_INFINITY
for (const [x, y] of pts) {
if (x < minx) minx = x
if (x > maxx) maxx = x
if (y < miny) miny = y
if (y > maxy) maxy = y
}
const w = maxx - minx || 1
const h = maxy - miny || 1
const scale = TARGET / Math.max(w, h)
const ox = 50 - ((minx + maxx) / 2) * scale
const oy = 50 - ((miny + maxy) / 2) * scale
const tx = (p: Pt): Pt => [ox + p[0] * scale, oy + p[1] * scale]
return edges.map(([A, B]) => [tx(A), tx(B)] as Edge)
}
function edgesToD(edges: Edge[]): string {
let d = ''
for (const [A, B] of edges) {
d += `M${A[0].toFixed(2)} ${A[1].toFixed(2)} L${B[0].toFixed(2)} ${B[1].toFixed(2)} `
}
return d.trim()
}
export interface IsoCubeGridProps {
size?: number
className?: string
forceHover?: boolean
}
export function IsoCubeGrid({ size = 110, className, forceHover = false }: IsoCubeGridProps) {
const { current, bind } = useGooMark<GridState>({ rest: REST, hover: HOVER, forceHover })
const { gradId, gooId } = useMarkIds()
const edges = normalizeEdges(buildEdges(current))
const { from, to } = gradientForTone(current.tone)
return (
<svg
viewBox='0 0 100 100'
width={size}
height={size}
role='img'
aria-label='Cube grid'
className={cn(
'focus-visible:outline-none focus-visible:ring-[1.5px] focus-visible:ring-[var(--brand-agent)]',
className
)}
style={{ display: 'block' }}
{...bind}
>
<GooDefs gradId={gradId} gooId={gooId} gooFusion={GOO_FUSION} from={from} to={to} />
<g
filter={`url(#${gooId})`}
strokeWidth={STROKE}
strokeLinecap='round'
strokeLinejoin='round'
fill='none'
>
<path d={edgesToD(edges)} stroke={`url(#${gradId})`} />
</g>
</svg>
)
}
@@ -1,146 +0,0 @@
'use client'
import { cn } from '@sim/emcn'
import { GooDefs } from '@/app/(landing)/components/mothership/components/iso-marks/goo-defs'
import {
type Edge,
gradientForTone,
isoProject,
type MarkState,
type Pt,
TARGET,
useGooMark,
useMarkIds,
} from '@/app/(landing)/components/mothership/components/iso-marks/use-goo-mark'
/**
* Sim iso goo-mark: CUBE ROW.
* Three cubes set in a level row. Rest: an even row, all one size. Hover (read):
* each cube resizes to a different scale, like a live gauge re-leveling. No
* spin; the motion is pure isometric scale.
*/
interface RowState extends MarkState {
s0: number
s1: number
s2: number
tilt: number
tone: number
}
const REST: RowState = { s0: 0.62, s1: 0.62, s2: 0.62, tilt: 0.5, tone: 1 }
const HOVER: RowState = { s0: 0.95, s1: 0.55, s2: 0.8, tilt: 0.5, tone: 1 }
const SLOTS = 3
const SPREAD = 0.92
const STROKE = 2.4
const GOO_FUSION = 1.0
function cubeAt(cx: number, cy: number, half: number, ky: number): Edge[] {
const corner = (sx: number, sy: number, sz: number) =>
isoProject(cx + sx * half, cy + sy * half, sz * half, ky)
const c = [
corner(-1, -1, -1),
corner(1, -1, -1),
corner(1, 1, -1),
corner(-1, 1, -1),
corner(-1, -1, 1),
corner(1, -1, 1),
corner(1, 1, 1),
corner(-1, 1, 1),
]
const ed: [number, number][] = [
[0, 1],
[1, 2],
[2, 3],
[3, 0],
[4, 5],
[5, 6],
[6, 7],
[7, 4],
[0, 4],
[1, 5],
[2, 6],
[3, 7],
]
return ed.map(([a, b]) => [c[a], c[b]] as Edge)
}
function buildEdges(s: RowState): Edge[] {
const sizes = [s.s0, s.s1, s.s2]
const edges: Edge[] = []
for (let i = 0; i < SLOTS; i++) {
const t = (i - (SLOTS - 1) / 2) * SPREAD
edges.push(...cubeAt(t, -t, sizes[i], s.tilt))
}
return edges
}
function normalizeEdges(edges: Edge[]): Edge[] {
const pts = edges.flat()
let minx = Number.POSITIVE_INFINITY
let maxx = Number.NEGATIVE_INFINITY
let miny = Number.POSITIVE_INFINITY
let maxy = Number.NEGATIVE_INFINITY
for (const [x, y] of pts) {
if (x < minx) minx = x
if (x > maxx) maxx = x
if (y < miny) miny = y
if (y > maxy) maxy = y
}
const w = maxx - minx || 1
const h = maxy - miny || 1
const scale = TARGET / Math.max(w, h)
const ox = 50 - ((minx + maxx) / 2) * scale
const oy = 50 - ((miny + maxy) / 2) * scale
const tx = (p: Pt): Pt => [ox + p[0] * scale, oy + p[1] * scale]
return edges.map(([A, B]) => [tx(A), tx(B)] as Edge)
}
function edgesToD(edges: Edge[]): string {
let d = ''
for (const [A, B] of edges) {
d += `M${A[0].toFixed(2)} ${A[1].toFixed(2)} L${B[0].toFixed(2)} ${B[1].toFixed(2)} `
}
return d.trim()
}
export interface IsoCubeRowProps {
size?: number
className?: string
forceHover?: boolean
}
export function IsoCubeRow({ size = 110, className, forceHover = false }: IsoCubeRowProps) {
const { current, bind } = useGooMark<RowState>({ rest: REST, hover: HOVER, forceHover })
const { gradId, gooId } = useMarkIds()
const edges = normalizeEdges(buildEdges(current))
const { from, to } = gradientForTone(current.tone)
return (
<svg
viewBox='0 0 100 100'
width={size}
height={size}
role='img'
aria-label='Cube row'
className={cn(
'focus-visible:outline-none focus-visible:ring-[1.5px] focus-visible:ring-[var(--brand-agent)]',
className
)}
style={{ display: 'block' }}
{...bind}
>
<GooDefs gradId={gradId} gooId={gooId} gooFusion={GOO_FUSION} from={from} to={to} />
<g
filter={`url(#${gooId})`}
strokeWidth={STROKE}
strokeLinecap='round'
strokeLinejoin='round'
fill='none'
>
<path d={edgesToD(edges)} stroke={`url(#${gradId})`} />
</g>
</svg>
)
}
@@ -1,162 +0,0 @@
'use client'
import { cn } from '@sim/emcn'
import { GooDefs } from '@/app/(landing)/components/mothership/components/iso-marks/goo-defs'
import {
type Edge,
gradientForTone,
isoProject,
type MarkState,
type Pt,
rotate2,
TARGET,
useGooMark,
useMarkIds,
} from '@/app/(landing)/components/mothership/components/iso-marks/use-goo-mark'
/**
* Sim iso goo-mark: FOUR-BOX TWIST.
* Four wireframe boxes layered vertically, each rotated at a progressive angular
* offset so the stack twists into a rounded cluster. Rest: open and twisted,
* still. Hover (close + spin): gap collapses, twist unwinds, spins. An optional
* signal-blue accent box is off by default (the landing stays greyscale).
*/
interface FourBoxState extends MarkState {
gap: number
twist: number
spin: number
tilt: number
tone: number
}
const REST: FourBoxState = { gap: 1, twist: 11, spin: -0.38, tilt: 0.4, tone: 1 }
const HOVER: FourBoxState = { gap: 0, twist: 0, spin: -3.14, tilt: 0.4, tone: 1 }
const BOXES = 2
const STROKE = 2.4
const GOO_FUSION = 1.4
const BLUE = '#9FC6E8'
function boxEdges(s: number, ky: number, rot: number, zc: number): Edge[] {
const corner = (sx: number, sy: number, sz: number) => {
const [rx, ry] = rotate2(sx * s, sy * s, rot)
return isoProject(rx, ry, sz * s * 0.4 + zc, ky)
}
const c = [
corner(-1, -1, -1),
corner(1, -1, -1),
corner(1, 1, -1),
corner(-1, 1, -1),
corner(-1, -1, 1),
corner(1, -1, 1),
corner(1, 1, 1),
corner(-1, 1, 1),
]
const ed: [number, number][] = [
[0, 1],
[1, 2],
[2, 3],
[3, 0],
[4, 5],
[5, 6],
[6, 7],
[7, 4],
[0, 4],
[1, 5],
[2, 6],
[3, 7],
]
return ed.map(([a, b]) => [c[a], c[b]] as Edge)
}
function buildBoxes(c: FourBoxState): Edge[][] {
const twRad = (c.twist * Math.PI) / 180
const totalH = (BOXES - 1) * c.gap
const boxes: Edge[][] = []
for (let i = 0; i < BOXES; i++) {
const zc = i * c.gap - totalH / 2
const rot = c.spin * i * 0.5 + i * twRad
boxes.push(boxEdges(1.0, c.tilt, rot, zc))
}
return boxes
}
function normalizeBoxes(boxes: Edge[][]): Edge[][] {
const pts = boxes.flat().flat()
let minx = Number.POSITIVE_INFINITY
let maxx = Number.NEGATIVE_INFINITY
let miny = Number.POSITIVE_INFINITY
let maxy = Number.NEGATIVE_INFINITY
for (const [x, y] of pts) {
if (x < minx) minx = x
if (x > maxx) maxx = x
if (y < miny) miny = y
if (y > maxy) maxy = y
}
const w = maxx - minx || 1
const h = maxy - miny || 1
const scale = TARGET / Math.max(w, h)
const ox = 50 - ((minx + maxx) / 2) * scale
const oy = 50 - ((miny + maxy) / 2) * scale
const tx = (p: Pt): Pt => [ox + p[0] * scale, oy + p[1] * scale]
return boxes.map((bx) => bx.map(([A, B]) => [tx(A), tx(B)] as Edge))
}
function edgesToD(edges: Edge[]): string {
let d = ''
for (const [A, B] of edges) {
d += `M${A[0].toFixed(2)} ${A[1].toFixed(2)} L${B[0].toFixed(2)} ${B[1].toFixed(2)} `
}
return d.trim()
}
export interface IsoFourBoxProps {
size?: number
className?: string
forceHover?: boolean
/** Render one box (the 2nd from bottom) in the signal-blue accent. */
blueAccent?: boolean
}
export function IsoFourBox({
size = 110,
className,
forceHover = false,
blueAccent = false,
}: IsoFourBoxProps) {
const { current, bind } = useGooMark<FourBoxState>({ rest: REST, hover: HOVER, forceHover })
const { gradId, gooId } = useMarkIds()
const boxes = normalizeBoxes(buildBoxes(current))
const { from, to } = gradientForTone(current.tone)
const blueIdx = blueAccent ? 1 : -1
const normal: Edge[] = []
let blue: Edge[] = []
boxes.forEach((bx, i) => {
if (i === blueIdx) blue = bx
else normal.push(...bx)
})
return (
<svg
viewBox='0 0 100 100'
width={size}
height={size}
aria-hidden='true'
className={cn('block outline-none', className)}
{...bind}
>
<GooDefs gradId={gradId} gooId={gooId} gooFusion={GOO_FUSION} from={from} to={to} />
<g
filter={`url(#${gooId})`}
strokeWidth={STROKE}
strokeLinecap='round'
strokeLinejoin='round'
fill='none'
>
<path d={edgesToD(normal)} stroke={`url(#${gradId})`} />
{blue.length > 0 && <path d={edgesToD(blue)} stroke={BLUE} />}
</g>
</svg>
)
}
@@ -1,82 +0,0 @@
'use client'
import { cn } from '@sim/emcn'
import { GooDefs } from '@/app/(landing)/components/mothership/components/iso-marks/goo-defs'
import {
type Edge,
edgesToPaths,
isoProject,
type MarkState,
rotate2,
useGooMark,
useMarkIds,
} from '@/app/(landing)/components/mothership/components/iso-marks/use-goo-mark'
/**
* Sim iso goo-mark: GRID PLANE.
* A single flat lattice rotated 45deg and squashed into isometric.
* Rest: tilted, still. Hover (open + spin): tilt flattens, spins.
*/
interface GridState extends MarkState {
tilt: number
spin: number
}
const REST: GridState = { tilt: 0.5, spin: 0 }
const HOVER: GridState = { tilt: 0.42, spin: 1.2 }
const DIVISIONS = 4
const STROKE = 1.5
const GOO_FUSION = 0.8
function buildEdges(c: GridState): Edge[] {
const half = 40
const proj = (u: number, v: number) => {
const [ru, rv] = rotate2(u, v, c.spin)
return isoProject(ru * half, rv * half, 0, c.tilt)
}
const E: Edge[] = []
for (let i = 0; i <= DIVISIONS; i++) {
const v = -1 + (2 * i) / DIVISIONS
E.push([proj(-1, v), proj(1, v)])
}
for (let i = 0; i <= DIVISIONS; i++) {
const u = -1 + (2 * i) / DIVISIONS
E.push([proj(u, -1), proj(u, 1)])
}
return E
}
export interface IsoGridPlaneProps {
size?: number
className?: string
forceHover?: boolean
}
export function IsoGridPlane({ size = 110, className, forceHover = false }: IsoGridPlaneProps) {
const { current, bind } = useGooMark<GridState>({ rest: REST, hover: HOVER, forceHover })
const { gradId, gooId } = useMarkIds()
return (
<svg
viewBox='0 0 100 100'
width={size}
height={size}
aria-hidden='true'
className={cn('block outline-none', className)}
{...bind}
>
<GooDefs gradId={gradId} gooId={gooId} gooFusion={GOO_FUSION} />
<g
filter={`url(#${gooId})`}
stroke={`url(#${gradId})`}
strokeWidth={STROKE}
strokeLinecap='round'
strokeLinejoin='round'
fill='none'
>
<path d={edgesToPaths(buildEdges(current))} />
</g>
</svg>
)
}
@@ -1,127 +0,0 @@
'use client'
import { cn } from '@sim/emcn'
import { GooDefs } from '@/app/(landing)/components/mothership/components/iso-marks/goo-defs'
import {
type Edge,
edgesToPaths,
gradientForTone,
isoProject,
type MarkState,
rotate2,
useGooMark,
useMarkIds,
} from '@/app/(landing)/components/mothership/components/iso-marks/use-goo-mark'
/**
* Sim iso goo-mark: STACKED PLANES.
* N lattice sheets layered with a vertical gap.
* Rest: open and spread, slightly tilted, still. Hover (close + spin): gap
* collapses tight, tilt steepens, spins.
*/
interface StackState extends MarkState {
gap: number
tilt: number
spin: number
stroke: number
gradCx: number
gradCy: number
gradR: number
tone: number
}
const REST: StackState = {
gap: 34.5,
tilt: 0.34,
spin: -2.82,
stroke: 2,
gradCx: 50,
gradCy: 50,
gradR: 44,
tone: 1,
}
const HOVER: StackState = {
gap: 11.5,
tilt: 0.33,
spin: -3.14,
stroke: 2,
gradCx: 50,
gradCy: 50,
gradR: 44,
tone: 1,
}
const PLANES = 4
const DIVISIONS = 2
const GOO_FUSION = 1.1
function buildEdges(c: StackState): Edge[] {
const half = 40
const totalH = (PLANES - 1) * c.gap
const proj = (u: number, v: number, z: number) => {
const [ru, rv] = rotate2(u, v, c.spin)
const p = isoProject(ru * half, rv * half, 0, c.tilt)
return [p[0], p[1] + (z - totalH / 2)] as [number, number]
}
const E: Edge[] = []
for (let pl = 0; pl < PLANES; pl++) {
const z = pl * c.gap
for (let i = 0; i <= DIVISIONS; i++) {
const v = -1 + (2 * i) / DIVISIONS
E.push([proj(-1, v, z), proj(1, v, z)])
}
for (let i = 0; i <= DIVISIONS; i++) {
const u = -1 + (2 * i) / DIVISIONS
E.push([proj(u, -1, z), proj(u, 1, z)])
}
}
return E
}
export interface IsoStackedPlanesProps {
size?: number
className?: string
forceHover?: boolean
}
export function IsoStackedPlanes({
size = 110,
className,
forceHover = false,
}: IsoStackedPlanesProps) {
const { current, bind } = useGooMark<StackState>({ rest: REST, hover: HOVER, forceHover })
const { gradId, gooId } = useMarkIds()
const { from, to } = gradientForTone(current.tone)
return (
<svg
viewBox='0 0 100 100'
width={size}
height={size}
aria-hidden='true'
className={cn('block outline-none', className)}
{...bind}
>
<GooDefs
gradId={gradId}
gooId={gooId}
gooFusion={GOO_FUSION}
from={from}
to={to}
cx={current.gradCx}
cy={current.gradCy}
r={current.gradR}
/>
<g
filter={`url(#${gooId})`}
stroke={`url(#${gradId})`}
strokeWidth={current.stroke}
strokeLinecap='round'
strokeLinejoin='round'
fill='none'
>
<path d={edgesToPaths(buildEdges(current))} />
</g>
</svg>
)
}
@@ -1,122 +0,0 @@
'use client'
import { cn } from '@sim/emcn'
import { GooDefs } from '@/app/(landing)/components/mothership/components/iso-marks/goo-defs'
import {
type Edge,
edgesToPaths,
isoProject,
type MarkState,
type Pt,
useGooMark,
useMarkIds,
} from '@/app/(landing)/components/mothership/components/iso-marks/use-goo-mark'
/**
* Sim iso goo-mark: ISO STAR.
* Three cuboid bars crossing at 0 / +60 / -60 degrees forming a 6-point
* interlocking star. Rest: thin bars, still. Hover (open + spin): bars thicken,
* spins.
*/
interface StarState extends MarkState {
thickness: number
spin: number
}
const REST: StarState = { thickness: 5, spin: 0 }
const HOVER: StarState = { thickness: 13, spin: 1.2 }
const BAR_LENGTH = 16
const STROKE = 1.5
const GOO_FUSION = 0.8
function rotZ(p: [number, number, number], a: number): [number, number, number] {
const c = Math.cos(a)
const s = Math.sin(a)
return [p[0] * c - p[1] * s, p[0] * s + p[1] * c, p[2]]
}
function barEdges(L: number, T: number): [number, number, number][][] {
const C = (sx: number, sy: number, sz: number): [number, number, number] => [
sx * L,
sy * T,
sz * T,
]
const corners: [number, number, number][] = [
[-1, -1, -1],
[1, -1, -1],
[1, 1, -1],
[-1, 1, -1],
[-1, -1, 1],
[1, -1, 1],
[1, 1, 1],
[-1, 1, 1],
]
const ci = (s: [number, number, number]) => C(s[0], s[1], s[2])
const ed: [number, number][] = [
[0, 1],
[1, 2],
[2, 3],
[3, 0],
[4, 5],
[5, 6],
[6, 7],
[7, 4],
[0, 4],
[1, 5],
[2, 6],
[3, 7],
]
return ed.map(([a, b]) => [ci(corners[a]), ci(corners[b])])
}
function buildEdges(c: StarState): Edge[] {
const L = BAR_LENGTH * 0.5
const T = c.thickness * 0.5
const angs = [c.spin, Math.PI / 3 + c.spin, -Math.PI / 3 + c.spin]
const E: Edge[] = []
for (const a of angs) {
for (const [p, q] of barEdges(L, T)) {
const pr = rotZ(p, a)
const qr = rotZ(q, a)
const A: Pt = isoProject(pr[0], pr[1], 1 + pr[2], 1)
const B: Pt = isoProject(qr[0], qr[1], 1 + qr[2], 1)
E.push([A, B])
}
}
return E
}
export interface IsoStarProps {
size?: number
className?: string
forceHover?: boolean
}
export function IsoStar({ size = 110, className, forceHover = false }: IsoStarProps) {
const { current, bind } = useGooMark<StarState>({ rest: REST, hover: HOVER, forceHover })
const { gradId, gooId } = useMarkIds()
return (
<svg
viewBox='0 0 100 100'
width={size}
height={size}
aria-hidden='true'
className={cn('block outline-none', className)}
{...bind}
>
<GooDefs gradId={gradId} gooId={gooId} gooFusion={GOO_FUSION} />
<g
filter={`url(#${gooId})`}
stroke={`url(#${gradId})`}
strokeWidth={STROKE}
strokeLinecap='round'
strokeLinejoin='round'
fill='none'
>
<path d={edgesToPaths(buildEdges(current))} />
</g>
</svg>
)
}
@@ -5,8 +5,8 @@ import { cn } from '@sim/emcn'
import { HeroWorkflowStage } from '@/app/(landing)/components/hero/components/hero-platform-loop/hero-workflow-stage'
import type { BlockDef } from '@/app/(landing)/components/hero/components/hero-visual/workflow-data'
import { HeroLoopShell } from '@/app/(landing)/components/shared/hero-loop-shell'
import { PLATFORM_LOOP_RESET_FADE_MS } from '@/app/(landing)/components/shared/platform-loop-constants'
import type { EnterpriseSidebarProps } from '@/app/(landing)/enterprise/components/enterprise-platform-loop/enterprise-sidebar'
import { RESET_FADE_MS } from '@/app/(landing)/hooks/use-design-scale'
import { useMotionSafeCycle } from '@/app/(landing)/hooks/use-motion-safe-cycle'
/** The empty canvas holds this long before the first block lands. */
@@ -32,8 +32,8 @@ export interface EditorLoopContent {
canvas: { width: number; height: number }
/** The block the "editing" beat selects once the flow is assembled. */
selectedBlockId: string
/** Workspace-nav row to highlight in the sidebar; unset keeps New chat active. */
activeNav?: EnterpriseSidebarProps['activeNav']
/** Sidebar row to highlight; unset keeps New chat active. */
activeItem?: EnterpriseSidebarProps['activeItem']
}
interface EditorLoopProps {
@@ -78,7 +78,7 @@ export function EditorLoop({ content }: EditorLoopProps) {
setTimeout(() => setBuiltCount(i + 1), IDLE_HOLD_MS + i * BUILD_STEP_MS)
),
setTimeout(() => setSelected(true), selectAt),
setTimeout(() => setFading(true), totalMs - RESET_FADE_MS),
setTimeout(() => setFading(true), totalMs - PLATFORM_LOOP_RESET_FADE_MS),
],
totalMs,
}
@@ -96,7 +96,7 @@ export function EditorLoop({ content }: EditorLoopProps) {
<HeroLoopShell
chats={content.sidebarChats}
workflows={content.sidebarWorkflows}
activeNav={content.activeNav}
activeItem={content.activeItem}
>
<div className='h-full w-full overflow-hidden rounded-[6px] border border-[var(--border)] bg-[var(--bg)]'>
<div
@@ -1,60 +1,61 @@
'use client'
import type { ReactNode } from 'react'
import { PLATFORM_LOOP_DESIGN } from '@/app/(landing)/components/shared/platform-loop-constants'
import {
EnterpriseSidebar,
type EnterpriseSidebarProps,
} from '@/app/(landing)/enterprise/components/enterprise-platform-loop/enterprise-sidebar'
import { DESIGN, useDesignScale } from '@/app/(landing)/hooks/use-design-scale'
interface HeroLoopShellProps {
/** Workspace name in the sidebar header chip. */
workspaceName?: string
/** Viewer name shown in the sidebar profile footer. */
profileName?: string
/** Recent-chat entries in the sidebar - four fill the design height. */
chats: readonly string[]
/** Deployed-workflow entries in the sidebar - five fill the design height. */
workflows: readonly string[]
/** Workspace-nav row to highlight; unset keeps New chat active. */
activeNav?: EnterpriseSidebarProps['activeNav']
/** Sidebar row to highlight; unset keeps New chat active. */
activeItem?: EnterpriseSidebarProps['activeItem']
/** The workspace pane's contents, rendered inside the inset pane gutter. */
children: ReactNode
}
/**
* The platform heroes' shared scaled stage: a `pointer-events-none` region
* whose fixed 1280x735 design-space layer is fitted to the rendered width via
* {@link useDesignScale} (`ResizeObserver` + `transform: scale`), holding the
* live {@link EnterpriseSidebar} beside the workspace pane each loop supplies
* as children. Purely presentational - the hero that renders it owns the
* `aria-hidden` frame and the animation clock.
* The platform heroes' shared scaled stage. An SVG viewBox maps the fixed
* 1280x735 design space to the rendered window without applying a CSS
* transform to the whole app. Keeping that scale out of the animated HTML
* subtree prevents fractional repaint snapping in both the canvas and the
* otherwise-static {@link EnterpriseSidebar}.
*/
export function HeroLoopShell({
workspaceName = 'Brightwave',
profileName = 'Morgan',
chats,
workflows,
activeNav,
activeItem,
children,
}: HeroLoopShellProps) {
const { regionRef, scale } = useDesignScale()
return (
<div ref={regionRef} className='pointer-events-none absolute inset-0 overflow-hidden'>
<div
className='flex origin-top-left bg-[var(--surface-1)]'
style={{
width: DESIGN.width,
height: DESIGN.height,
transform: `scale(${scale})`,
}}
>
<EnterpriseSidebar
workspaceName={workspaceName}
chats={chats}
workflows={workflows}
activeNav={activeNav}
/>
<div className='h-full min-w-0 flex-1 py-[7px] pr-[8px]'>{children}</div>
</div>
</div>
<svg
aria-hidden='true'
className='pointer-events-none absolute inset-0 size-full overflow-hidden'
viewBox={`0 0 ${PLATFORM_LOOP_DESIGN.width} ${PLATFORM_LOOP_DESIGN.height}`}
preserveAspectRatio='xMinYMin meet'
>
<foreignObject width={PLATFORM_LOOP_DESIGN.width} height={PLATFORM_LOOP_DESIGN.height}>
<div className='flex size-full bg-[var(--surface-1)]'>
<EnterpriseSidebar
workspaceName={workspaceName}
profileName={profileName}
chats={chats}
workflows={workflows}
activeItem={activeItem}
/>
<div className='h-full min-w-0 flex-1 py-[7px] pr-[8px]'>{children}</div>
</div>
</foreignObject>
</svg>
)
}
@@ -0,0 +1,5 @@
/** Fixed design space shared by every live landing platform preview. */
export const PLATFORM_LOOP_DESIGN = { width: 1280, height: 735 } as const
/** Fade-out length before a platform preview restarts its animation cycle. */
export const PLATFORM_LOOP_RESET_FADE_MS = 300
@@ -0,0 +1,7 @@
export const PREVIEW_SIDEBAR_CHATS = ['Enrich new signups', 'Post deal alerts to #sales'] as const
export const PREVIEW_SIDEBAR_WORKFLOWS = [
'Lead enrichment',
'Inbound lead routing',
'Weekly pipeline report',
] as const
@@ -21,7 +21,7 @@ export function SolutionsCardRowHeader({ row, headingId }: SolutionsCardRowHeade
<div className='flex flex-col items-start gap-3 text-left'>
<h2
id={headingId}
className='max-w-[540px] text-balance font-medium text-[22px] text-[var(--text-primary)] leading-[1.3] max-sm:text-[20px]'
className='max-w-[540px] text-balance text-[22px] text-[var(--text-primary)] leading-[1.3] max-sm:text-[20px]'
>
{row.title}
</h2>
@@ -4,6 +4,7 @@ import { useMemo, useState } from 'react'
import { cn } from '@sim/emcn'
import { HeroWorkflowStage } from '@/app/(landing)/components/hero/components/hero-platform-loop/hero-workflow-stage'
import { HeroLoopShell } from '@/app/(landing)/components/shared/hero-loop-shell'
import { PLATFORM_LOOP_RESET_FADE_MS } from '@/app/(landing)/components/shared/platform-loop-constants'
import { EnterpriseHomeStage } from '@/app/(landing)/enterprise/components/enterprise-platform-loop/enterprise-home-stage'
import {
BUILD_STEP_MS,
@@ -12,7 +13,6 @@ import {
type EnterpriseLoopContent,
type EnterpriseLoopPhase,
} from '@/app/(landing)/enterprise/components/enterprise-platform-loop/stage-data'
import { RESET_FADE_MS } from '@/app/(landing)/hooks/use-design-scale'
import { useMotionSafeCycle } from '@/app/(landing)/hooks/use-motion-safe-cycle'
interface EnterprisePlatformLoopProps {
@@ -78,7 +78,7 @@ export function EnterprisePlatformLoop({
setTimeout(() => setBuiltCount(i + 1), timeline.buildStart + i * BUILD_STEP_MS)
),
setTimeout(() => setPhase('reply'), timeline.reply),
setTimeout(() => setFading(true), timeline.total - RESET_FADE_MS),
setTimeout(() => setFading(true), timeline.total - PLATFORM_LOOP_RESET_FADE_MS),
],
totalMs: timeline.total,
}
@@ -96,6 +96,7 @@ export function EnterprisePlatformLoop({
return (
<HeroLoopShell
workspaceName={content.workspaceName}
profileName={content.profileName}
chats={content.sidebarChats}
workflows={content.sidebarWorkflows}
>
@@ -1,16 +1,22 @@
import { memo } from 'react'
import { ChevronDown, cn, Home, Library } from '@sim/emcn'
import {
Calendar,
ChipChevronDown,
chipContentIconClass,
chipContentLabelClass,
chipVariants,
cn,
} from '@sim/emcn'
import {
Database,
File,
Files,
HelpCircle,
Home,
Integration,
Library,
MoreHorizontal,
PanelLeft,
Plus,
Search,
Settings,
Table,
} from '@sim/emcn/icons'
import Image from 'next/image'
@@ -21,12 +27,13 @@ import {
const WORKSPACE_NAV = [
{ label: 'Tables', icon: Table },
{ label: 'Files', icon: File },
{ label: 'Knowledge base', icon: Database },
{ label: 'Scheduled tasks', icon: Calendar },
{ label: 'Files', icon: Files },
{ label: 'Knowledge bases', icon: Database },
{ label: 'Logs', icon: Library },
] as const
export type SidebarItem = 'New chat' | 'Integrations' | (typeof WORKSPACE_NAV)[number]['label']
interface IconRowProps {
icon: React.ComponentType<{ className?: string }>
label: string
@@ -36,14 +43,9 @@ interface IconRowProps {
/** A sidebar nav row with a leading icon, like the real workspace sidebar. */
function IconRow({ icon: Icon, label, active = false }: IconRowProps) {
return (
<div
className={cn(
'mx-0.5 flex h-[28px] items-center gap-2 rounded-[8px] px-2',
active && 'bg-[var(--surface-active)]'
)}
>
<Icon className='size-[14px] flex-shrink-0 text-[var(--text-icon)]' />
<span className='truncate text-[13px] text-[var(--text-body)]'>{label}</span>
<div className={chipVariants({ active, fullWidth: true })}>
<Icon className={chipContentIconClass} />
<span className={chipContentLabelClass}>{label}</span>
</div>
)
}
@@ -51,8 +53,8 @@ function IconRow({ icon: Icon, label, active = false }: IconRowProps) {
/** A bare text row - the real sidebar's chat and workflow entries. */
function TextRow({ label }: { label: string }) {
return (
<div className='mx-0.5 flex h-[28px] items-center rounded-[8px] px-2'>
<span className='truncate text-[13px] text-[var(--text-body)]'>{label}</span>
<div className={chipVariants({ fullWidth: true })}>
<span className={chipContentLabelClass}>{label}</span>
</div>
)
}
@@ -61,7 +63,7 @@ function TextRow({ label }: { label: string }) {
function SectionLabel({ label, actions }: { label: string; actions?: boolean }) {
return (
<div className='flex items-center justify-between px-4 pb-1.5'>
<span className='text-[12px] text-[var(--text-icon)]'>{label}</span>
<span className='text-[var(--text-muted)] text-caption'>{label}</span>
{actions && (
<span className='flex items-center gap-2 text-[var(--text-icon)]'>
<MoreHorizontal className='size-[14px]' />
@@ -75,46 +77,39 @@ function SectionLabel({ label, actions }: { label: string; actions?: boolean })
export interface EnterpriseSidebarProps {
/** Workspace name in the header chip. Defaults to the enterprise workspace. */
workspaceName?: string
/** Viewer name shown in the profile footer. Defaults to the enterprise persona. */
profileName?: string
/** Recent-chat entries - four fill the design height. Defaults enterprise. */
chats?: readonly string[]
/** Deployed-workflow entries - five fill the design height. Defaults enterprise. */
workflows?: readonly string[]
/**
* Workspace-nav row to render active (e.g. `'Tables'`) - the platform pages
* highlight their own module instead of New chat. Unset keeps the enterprise
* default (New chat active).
*/
activeNav?: (typeof WORKSPACE_NAV)[number]['label']
/** Sidebar row to render active. Defaults to New chat. */
activeItem?: SidebarItem
}
/**
* The Brightwave workspace sidebar, rendered live (the homepage loop keeps its
* baked-screenshot sidebar; the enterprise loop draws its own so the content
* can read like a large tenured deployment): the workspace header, New chat /
* Search / Integrations, a filled-out Chats history, the Workspace nav, a full
* Workflows section, and the Help / Settings footer. Purely decorative -
* The Brightwave workspace sidebar, rendered live across landing previews so
* every surface stays aligned with the product: the workspace header, New chat /
* Integrations, a filled-out Chats history, the Workspace nav, a full
* Workflows section, and the profile / Help footer. Purely decorative -
* hover/click behavior is owned by the parent's `pointer-events-none` frame.
* The workspace name and the chat / workflow entries are injectable so each
* solutions hero can read like that team's workspace; defaults keep the
* The workspace and profile names plus the chat / workflow entries are injectable
* so each solutions hero can read like that team's workspace; defaults keep the
* enterprise page exactly as it renders today. Memoized - the sidebar is
* fully static per props, and every consuming loop re-renders on each clock
* tick with stable sidebar props.
*/
export const EnterpriseSidebar = memo(function EnterpriseSidebar({
workspaceName = 'Brightwave',
profileName = 'Morgan',
chats = SIDEBAR_CHATS,
workflows = SIDEBAR_WORKFLOWS,
activeNav,
activeItem = 'New chat',
}: EnterpriseSidebarProps = {}) {
return (
<div className='flex h-full w-[249px] flex-shrink-0 flex-col bg-[var(--surface-1)] pt-3'>
{/* Workspace header, matching the real product's WorkspaceHeader chip
(borderless `chipVariants()` geometry: h-[30px] rounded-lg px-2 with
mx-0.5, 16px logo, text-sm name, 14px chevron) and therefore the
homepage's baked sidebar pixels - logo + name + chevron as a bare
row, panel toggle right-aligned outside it. */}
<div className='isolate flex h-full w-[238px] flex-shrink-0 flex-col bg-[var(--surface-1)] pt-3 will-change-transform'>
<div className='flex flex-shrink-0 items-center justify-between px-2'>
<div className='mx-0.5 flex h-[30px] min-w-0 items-center gap-2 rounded-lg px-2'>
<div className={cn(chipVariants(), 'min-w-0 flex-1')}>
{/* The exact Brightwave mark the homepage capture seeds
(`readme-tour-capture` sets `logoUrl: '/landing/rivian-logo.svg'`),
so both platform previews show the same company logo. */}
@@ -125,53 +120,68 @@ export const EnterpriseSidebar = memo(function EnterpriseSidebar({
height={16}
className='size-[16px] flex-shrink-0 rounded-sm'
/>
<span className='min-w-0 truncate text-[var(--text-body)] text-sm'>{workspaceName}</span>
<ChevronDown className='size-[14px] flex-shrink-0 text-[var(--text-icon)]' />
<span className={chipContentLabelClass}>{workspaceName}</span>
<ChipChevronDown />
</div>
<div className='flex h-[30px] w-[65px] flex-shrink-0 items-center gap-[1px]'>
<span className={chipVariants()}>
<Search className={chipContentIconClass} />
</span>
<span className={chipVariants()}>
<PanelLeft className={chipContentIconClass} />
</span>
</div>
<PanelLeft className='mr-1.5 size-[16px] flex-shrink-0 text-[var(--text-icon)]' />
</div>
<div className='mt-2.5 flex flex-shrink-0 flex-col gap-0.5 px-2'>
<IconRow icon={Home} label='New chat' active={!activeNav} />
<IconRow icon={Search} label='Search' />
<IconRow icon={Integration} label='Integrations' />
<div className='mt-4 flex flex-shrink-0 flex-col gap-[1px] px-2'>
<IconRow icon={Home} label='New chat' active={activeItem === 'New chat'} />
<IconRow icon={Integration} label='Integrations' active={activeItem === 'Integrations'} />
</div>
<div className='mt-3.5 flex flex-shrink-0 flex-col'>
<div className='mt-4 flex flex-shrink-0 flex-col'>
<SectionLabel label='Chats' />
<div className='flex flex-col gap-0.5 px-2'>
<div className='flex flex-col gap-[1px] px-2'>
{chats.map((chat) => (
<TextRow key={chat} label={chat} />
))}
</div>
</div>
<div className='mt-3.5 flex flex-shrink-0 flex-col'>
<div className='mt-4 flex flex-shrink-0 flex-col'>
<SectionLabel label='Workspace' />
<div className='flex flex-col gap-0.5 px-2'>
<div className='flex flex-col gap-[1px] px-2'>
{WORKSPACE_NAV.map((item) => (
<IconRow
key={item.label}
icon={item.icon}
label={item.label}
active={item.label === activeNav}
active={item.label === activeItem}
/>
))}
</div>
</div>
<div className='flex min-h-0 flex-1 flex-col overflow-hidden pt-3.5'>
<div className='flex min-h-0 flex-1 flex-col overflow-hidden pt-4'>
<SectionLabel label='Workflows' actions />
<div className='flex flex-col gap-0.5 px-2'>
<div className='flex flex-col gap-[1px] px-2'>
{workflows.map((workflow) => (
<TextRow key={workflow} label={workflow} />
))}
</div>
</div>
<div className='flex flex-shrink-0 flex-col gap-0.5 px-2 pt-[9px] pb-2'>
<IconRow icon={HelpCircle} label='Help' />
<IconRow icon={Settings} label='Settings' />
<div className='flex flex-shrink-0 items-center border-t px-2 pt-[9px] pb-2'>
<div className='flex min-w-0 flex-1'>
<div className={cn(chipVariants(), 'min-w-0 max-w-full')}>
<span className='flex size-[16px] flex-shrink-0 items-center justify-center rounded-full bg-[var(--surface-4)] text-[var(--text-body)] text-micro leading-none'>
{profileName.charAt(0).toUpperCase()}
</span>
<span className={chipContentLabelClass}>{profileName}</span>
</div>
</div>
<span className={cn(chipVariants(), 'flex-shrink-0')}>
<HelpCircle className={chipContentIconClass} />
</span>
</div>
</div>
)
@@ -157,6 +157,8 @@ export type EnterpriseLoopPhase = 'idle' | 'typing' | 'typed' | 'dispatch' | 're
export interface EnterpriseLoopContent {
/** Workspace name shown in the sidebar header. */
workspaceName: string
/** Viewer name shown in the sidebar profile footer. */
profileName: string
/** The new-chat greeting, personalized like the real workspace Home. */
greeting: string
/** Composer placeholder shown before the prompt types out. */
@@ -182,6 +184,7 @@ export interface EnterpriseLoopContent {
/** The enterprise hero's own loop content - the parametrized loop's default. */
export const ENTERPRISE_LOOP_CONTENT: EnterpriseLoopContent = {
workspaceName: 'Brightwave',
profileName: 'Morgan',
greeting: ENTERPRISE_GREETING,
placeholder: COMPOSER_PLACEHOLDER,
prompt: ENTERPRISE_PROMPT,
@@ -7,8 +7,8 @@ import { ArrowUpDown, File, ListFilter, Plus, Search } from '@sim/emcn/icons'
import { AgentIcon } from '@/components/icons'
import { CsvIcon, DocxIcon, PdfIcon } from '@/components/icons/document-icons'
import { HeroLoopShell } from '@/app/(landing)/components/shared/hero-loop-shell'
import { PLATFORM_LOOP_RESET_FADE_MS } from '@/app/(landing)/components/shared/platform-loop-constants'
import { ZipIcon } from '@/app/(landing)/components/shared/zip-icon'
import { RESET_FADE_MS } from '@/app/(landing)/hooks/use-design-scale'
import { useMotionSafeCycle } from '@/app/(landing)/hooks/use-motion-safe-cycle'
/** Sidebar content for the files hero - a file-heavy team's workspace. */
@@ -222,7 +222,7 @@ export function FilesHeroLoop() {
setTimeout(() => setRowCount(i + 1), IDLE_HOLD_MS + i * ROW_STEP_MS)
),
setTimeout(() => setDropped(true), dropAt),
setTimeout(() => setFading(true), totalMs - RESET_FADE_MS),
setTimeout(() => setFading(true), totalMs - PLATFORM_LOOP_RESET_FADE_MS),
],
totalMs,
}
@@ -235,7 +235,7 @@ export function FilesHeroLoop() {
})
return (
<HeroLoopShell chats={SIDEBAR_CHATS} workflows={SIDEBAR_WORKFLOWS} activeNav='Files'>
<HeroLoopShell chats={SIDEBAR_CHATS} workflows={SIDEBAR_WORKFLOWS} activeItem='Files'>
<div className='h-full w-full overflow-hidden rounded-[6px] border border-[var(--border)] bg-[var(--bg)]'>
<div
className={cn(
@@ -1,40 +0,0 @@
'use client'
import { useLayoutEffect, useRef, useState } from 'react'
/**
* The platform heroes' shared design space - the 1280x735 "mini app" geometry
* every hero loop lays out in (matching the homepage capture's CSS layout),
* so each hero reads at the identical scale inside the shared demo window.
*/
export const DESIGN = { width: 1280, height: 735 } as const
/** Fade-out length before a hero loop's cycle restarts. */
export const RESET_FADE_MS = 300
/**
* Tracks the rendered region's width via `ResizeObserver` and derives the
* scale that fits the {@link DESIGN}-space layer to it, keeping the live
* layer's proportions locked to the window's. Attach `regionRef` to the
* loop's outer region and apply `transform: scale(${scale})` to the
* design-space layer.
*/
export function useDesignScale() {
const regionRef = useRef<HTMLDivElement>(null)
const [scale, setScale] = useState(1)
useLayoutEffect(() => {
const el = regionRef.current
if (!el) return
const measure = () => {
const w = el.getBoundingClientRect().width
if (w > 40) setScale(w / DESIGN.width)
}
measure()
const ro = new ResizeObserver(measure)
ro.observe(el)
return () => ro.disconnect()
}, [])
return { regionRef, scale }
}
@@ -1,5 +1,6 @@
import { ChipLink } from '@sim/emcn'
import type { Metadata } from 'next'
import { StatusPage } from '@/components/status-page'
export const metadata: Metadata = {
title: 'Page Not Found',
@@ -8,19 +9,13 @@ export const metadata: Metadata = {
export default function IntegrationsNotFound() {
return (
<main
id='main-content'
className='mx-auto flex min-h-[60vh] w-full max-w-[1460px] flex-col items-center justify-center gap-3 px-20 py-24 text-center max-sm:px-5 max-lg:px-8'
<StatusPage
title='Integration not found'
description="The integration you're looking for doesn't exist or has been moved."
>
<h1 className='text-balance text-[40px] text-[var(--text-primary)] leading-[110%] tracking-[-0.02em]'>
Integration not found
</h1>
<p className='text-[var(--text-muted)] text-lg'>
The integration you&apos;re looking for doesn&apos;t exist or has been moved.
</p>
<ChipLink variant='primary' href='/integrations' className='mt-3'>
<ChipLink variant='primary' href='/integrations'>
Browse integrations
</ChipLink>
</main>
</StatusPage>
)
}
@@ -14,7 +14,7 @@ import {
ZendeskIcon,
} from '@/components/icons'
import { HeroLoopShell } from '@/app/(landing)/components/shared/hero-loop-shell'
import { RESET_FADE_MS } from '@/app/(landing)/hooks/use-design-scale'
import { PLATFORM_LOOP_RESET_FADE_MS } from '@/app/(landing)/components/shared/platform-loop-constants'
import { useMotionSafeCycle } from '@/app/(landing)/hooks/use-motion-safe-cycle'
/** Sidebar content for the knowledge hero - a team living in its docs. */
@@ -149,7 +149,7 @@ export function KnowledgeHeroLoop() {
),
setTimeout(() => setSyncPhase('syncing'), syncAt),
setTimeout(() => setSyncPhase('synced'), syncAt + SYNC_MS),
setTimeout(() => setFading(true), totalMs - RESET_FADE_MS),
setTimeout(() => setFading(true), totalMs - PLATFORM_LOOP_RESET_FADE_MS),
],
totalMs,
}
@@ -162,7 +162,7 @@ export function KnowledgeHeroLoop() {
})
return (
<HeroLoopShell chats={SIDEBAR_CHATS} workflows={SIDEBAR_WORKFLOWS} activeNav='Knowledge base'>
<HeroLoopShell chats={SIDEBAR_CHATS} workflows={SIDEBAR_WORKFLOWS} activeItem='Knowledge bases'>
<div className='h-full w-full overflow-hidden rounded-[6px] border border-[var(--border)] bg-[var(--bg)]'>
<div
className={cn(
@@ -1,5 +1,6 @@
import { ChipLink } from '@sim/emcn'
import type { Metadata } from 'next'
import { StatusPage } from '@/components/status-page'
export const metadata: Metadata = {
title: 'Page Not Found',
@@ -8,19 +9,13 @@ export const metadata: Metadata = {
export default function LibraryAuthorNotFound() {
return (
<main
id='main-content'
className='mx-auto flex min-h-[60vh] w-full max-w-[1460px] flex-col items-center justify-center gap-3 px-20 py-24 text-center max-sm:px-5 max-lg:px-8'
<StatusPage
title='Author not found'
description="The author you're looking for doesn't exist or has been moved."
>
<h1 className='text-balance text-[40px] text-[var(--text-primary)] leading-[110%] tracking-[-0.02em]'>
Author not found
</h1>
<p className='text-[var(--text-muted)] text-lg'>
The author you&apos;re looking for doesn&apos;t exist or has been moved.
</p>
<ChipLink variant='primary' href='/library' className='mt-3'>
<ChipLink variant='primary' href='/library'>
Browse library
</ChipLink>
</main>
</StatusPage>
)
}
+6 -11
View File
@@ -1,5 +1,6 @@
import { ChipLink } from '@sim/emcn'
import type { Metadata } from 'next'
import { StatusPage } from '@/components/status-page'
export const metadata: Metadata = {
title: 'Page Not Found',
@@ -8,19 +9,13 @@ export const metadata: Metadata = {
export default function LibraryNotFound() {
return (
<main
id='main-content'
className='mx-auto flex min-h-[60vh] w-full max-w-[1460px] flex-col items-center justify-center gap-3 px-20 py-24 text-center max-sm:px-5 max-lg:px-8'
<StatusPage
title='Post not found'
description="The post you're looking for doesn't exist or has been moved."
>
<h1 className='text-balance text-[40px] text-[var(--text-primary)] leading-[110%] tracking-[-0.02em]'>
Post not found
</h1>
<p className='text-[var(--text-muted)] text-lg'>
The post you&apos;re looking for doesn&apos;t exist or has been moved.
</p>
<ChipLink variant='primary' href='/library' className='mt-3'>
<ChipLink variant='primary' href='/library'>
Browse library
</ChipLink>
</main>
</StatusPage>
)
}

Some files were not shown because too many files have changed in this diff Show More