Compare commits

..

42 Commits

Author SHA1 Message Date
Waleed
c400e59ea6 feat(sap_s4hana): add get_material_document and fix supplier invoice key order (#4317)
* fix(sap_s4hana): require non-empty items in create_purchase_order

Why: SAP A_PurchaseOrder POST silently fails or returns opaque errors
without to_PurchaseOrderItem entries. Block already required this body
but the tool marked it optional and didn't validate items presence —
mismatched contract with create_sales_order / create_purchase_requisition.

Also clarifies the deliveryDocument placeholder to show both outbound
and inbound number ranges.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(sap_s4hana): add get_material_document and fix supplier invoice key order

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(sap_s4hana): align material doc key order in description and require purchase order body type

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 23:41:56 -07:00
Vikhyath Mondreti
2e3de9ac8a feat(governance): external workspace users from outside org (#4313)
* feat(governance): external workspace users from outside org

* update docs

* address comments

* edge case improvements

* remove unused fallback

* address comments

* add outbox for seat reduction

* fix edge case with org join after invite

* add server side batch invites for workspace

* use zod schema for route
2026-04-27 22:07:41 -07:00
Theodore Li
154b9d0883 fix(vm): categorize user or server side errors (#4283)
* fix(vm): categorize user or server side errors

* recategorize function syntax errors as 4xx
2026-04-27 22:27:50 -04:00
Waleed
c95ac3bc23 improvement(browser-use,stagehand): expose live session URLs (#4314)
* improvement(browser-use,stagehand): expose live session URLs and align with latest API specs

- Browser Use: switch to v2 camelCase schema, fetch live URL from sessions endpoint, add startUrl/maxSteps/allowedDomains/vision/flashMode/thinking/systemPromptExtension/structuredOutput/metadata params, surface liveUrl/shareUrl/sessionId outputs
- Stagehand: fetch Browserbase debug URL, add mode/maxSteps params, surface liveViewUrl/sessionId outputs, bump @browserbasehq/stagehand to ^3.2.1, update to claude-sonnet-4-6

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(browser-use): respect API default for highlightElements

Only send highlightElements when user explicitly toggles it; previously defaulted to true which silently overrode the v2 API default of false.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(browser-use,stagehand): address PR review feedback

- Browser Use: fetch liveUrl during polling once sessionId is known, instead of immediately after task creation. Handles tasks started without profile_id (where sessionId isn't returned in create response) and ensures session is active before fetching.
- Stagehand: coerce empty/whitespace maxSteps strings to undefined so they're dropped from the request body instead of failing zod validation as ''.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(stagehand): preserve liveViewUrl and sessionId on agent error

If the agent throws after Browserbase session init succeeds, callers can still surface the live view / session ID for debugging.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(browser-use): coerce empty maxSteps strings to undefined

Mirrors the Stagehand block's handling so a cleared field doesn't pass through as ''.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(browser-use): skip metadata when empty

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 18:02:51 -07:00
Theodore Li
ca814f021d fix(ui): display file upload error messages (#4315)
* fix(ui): display file upload messages

* Address pr comments
2026-04-27 18:58:41 -04:00
Waleed
2502369122 feat(integrations): SAP S/4HANA (#4301)
* feat(integrations): SAP S/4HANA tools, block, and proxy with multi-deployment support

* fix(sap_s4hana): address PR review comments

- Validate baseUrl/tokenUrl in Zod schema and at runtime to prevent SSRF
  (https-only, deny loopback/link-local/cloud-metadata hosts)
- Cap proxy token cache at 500 entries with LRU eviction
- Add 30s timeout to outbound token, CSRF, and OData fetches
- Make parseJsonInput return T | undefined so missing input is type-safe
- Reset authType when deploymentType changes and surface OAuth fields
  whenever auth is not basic, so cloud_public users always see clientId/
  clientSecret after switching from a basic-auth private deployment
- Reject OData service names that are not uppercase identifiers and
  paths containing ".." or "." traversal segments

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(sap_s4hana): allow versioned service names; tighten proxy SSRF defenses

- Permit ";v=NNNN" suffix on ServiceName regex so the four delivery tools
  (API_OUTBOUND_DELIVERY_SRV;v=0002, API_INBOUND_DELIVERY_SRV;v=0002) pass
  schema validation
- Restrict subdomain to RFC 1123 label characters and region to lowercase
  alphanumeric short codes; run the constructed cloud_public host through
  assertSafeExternalUrl so a crafted subdomain (e.g. "evil.com#") cannot
  redirect requests carrying SAP credentials
- Block RFC-1918 (10/8, 172.16/12, 192.168/16), 127/8, 169.254/16, and
  0.0.0.0 via isPrivateIPv4, plus IPv4-mapped IPv6 variants
  (::ffff:10.0.0.1, ::10.0.0.1) so private internal hosts cannot be
  reached from baseUrl, tokenUrl, or the resolved cloud_public URL

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(sap_s4hana): catch hex-form IPv4-mapped IPv6 in SSRF check

The WHATWG URL parser normalizes IPv4-mapped IPv6 addresses to hex form
(e.g. [::ffff:169.254.169.254] → [::ffff:a9fe:a9fe]), which slipped past
the dotted-decimal-only extractor. Decode the trailing two 16-bit hex
groups back into IPv4 octets and run them through isPrivateIPv4. Also
add isPrivateOrLoopbackIPv6 so pure IPv6 loopback (::, ::1), unique
local addresses (fc00::/7), and link-local (fe80::/10) cannot be reached.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(sap_s4hana): scope CSRF metadata fetch and isolate token cache by secret

- buildOdataUrl skips request query params when called with an internal
  pathOverride so the /$metadata CSRF probe never carries user OData
  options ($filter, $top, $select), which were causing write operations
  through the generic odata_query tool to fail.
- tokenCacheKey now mixes a sha256 hash of clientSecret into the cache
  key so two tenants sharing the same tokenUrl + clientId but different
  secrets get isolated entries (no cross-tenant token leak).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(sap_s4hana): reject ?/# in service path; trim long update tool descriptions

- ServicePath validator now rejects "?" and "#" so a caller can't smuggle
  query options through the path field (e.g.,
  "/A_BusinessPartner?$format=atomsvc"); the Zod refine now reports
  ".." / "." segments, "?", and "#" together.
- Update Customer / Update Supplier / Update Purchase Requisition tool
  descriptions exceeded the docs generator's 600-char regex window, so
  they were rendering with empty descriptions on the integrations
  landing page. Trimmed them to fit while keeping the limited-fields
  note and the If-Match guidance, then regenerated integrations.json
  and tool docs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(sap_s4hana): reject percent-encoded path traversal; widen Set-Cookie split

- ServicePath now also rejects %2e/%2E, %2f/%2F, %5c/%5C, %3f/%3F, %23
  so a caller cannot smuggle ".." / "." / "/" / "\" / "?" / "#" past the
  validator and have SAP's ABAP/ICM gateway decode them server-side.
- joinSetCookies fallback regex now allows the ", " separator that's
  used when multiple Set-Cookie values are folded onto one header line
  (older runtimes without Headers.getSetCookie). Prevents CSRF cookies
  from being concatenated into a single value during write operations.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(sap_s4hana): preserve $ in OData query params; reject empty items array

- buildOdataUrl now constructs query strings manually with
  encodeURIComponent and restores literal "$" so OData system options
  ($filter, $top, $select, $expand, $orderby, $skip, $format) reach
  SAP and any intermediary proxies/WAFs as-is, not as "%24filter".
  URLSearchParams was percent-encoding "$" to "%24" which most ICMs
  decode but some intermediaries silently drop, returning unfiltered
  results.
- create_sales_order now rejects an empty items array (matches
  create_purchase_requisition) so callers get a clear client-side
  error instead of an opaque SAP validation failure on the deep-insert.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(sap_s4hana): ignore baseUrl on cloud_public to prevent token redirection

Why: resolveHost previously preferred baseUrl unconditionally. A caller
sending deploymentType=cloud_public with a baseUrl pointing elsewhere
would obtain a real SAP UAA token, then forward it as Bearer to the
attacker host. Zod superRefine did not validate baseUrl for cloud_public.

Fix: resolveHost now constructs the SAP host from subdomain when
deploymentType is cloud_public and only uses baseUrl for cloud_private
and on_premise (where it is already SSRF-checked in superRefine).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(icons): use useId for SapS4HanaIcon and PipedriveIcon gradients

Why: hardcoded SVG gradient/mask IDs collide when an icon renders more
than once on a page (e.g. integrations listing). All other icons in this
file use React's useId() — these were inconsistent.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* icons

* fix(icons): use useId for AWS-style icon gradients

Why: IAMIcon, IdentityCenterIcon, STSIcon, SESIcon, and SecretsManagerIcon
all used hardcoded `id='xxxGradient'` values that collide when an icon
renders more than once on a page (e.g. integrations listing).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(sap_s4hana): ignore tokenUrl on cloud_public to prevent UAA redirection

Why: resolveTokenUrl previously honored caller-supplied tokenUrl
regardless of deploymentType, mirroring the same redirection class as
the prior baseUrl bug. A cloud_public caller could send tokenUrl to an
attacker host, causing the proxy to POST clientId:clientSecret as Basic
auth to it. superRefine for cloud_public did not validate tokenUrl.

Fix: derive UAA URL from subdomain+region for cloud_public; only honor
tokenUrl for cloud_private/on_premise (already SSRF-checked).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(icons): remove unused mask in PipedriveIcon

Why: the <mask> element had no consumer (no mask='url(#...)' anywhere
in the SVG), so both it and the maskId variable were dead code.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 15:07:00 -07:00
Waleed
8266f0afdb fix(security): rate limit chat OTP + validate mothership proxy endpoint (#4312)
* fix(security): rate limit chat OTP endpoint to prevent email bombing

* fix(security): validate mothership proxy endpoint to block path traversal

* fix(security): address greptile feedback on OTP rate limiting
2026-04-27 14:51:58 -07:00
Waleed
896a00ae31 fix(security): require internal API key for copilot training endpoints (#4311) 2026-04-27 14:30:47 -07:00
Waleed
74946fb162 improvement(docker): speed up app image build with cache mounts and parallel node-gyp (#4310) 2026-04-27 13:34:57 -07:00
Waleed
f62d274478 fix(mothership): stabilize task sidebar ordering on selection (#4309) 2026-04-27 12:42:20 -07:00
Theodore Li
65e17de065 fix(retention-job): add chunking strategy for cleanup (#4305)
* fix(retention-job): add chunking strategy for cleanup

* change stats to be perjob not per chunk
2026-04-27 15:13:00 -04:00
Vikhyath Mondreti
79ff5d80b3 improvement(slack): channel selector for list canvasses (#4307) 2026-04-27 12:11:14 -07:00
Vikhyath Mondreti
2a52141d2f feat(slack): canvas related operations (#4306)
* feat(slack): canvas related operations

* extract shared code
2026-04-27 12:00:40 -07:00
Theodore Li
76ad59fd7d fix(stream): Avoid bun memory leak bug from TransformStream (#4255)
* Avoid bun memory leak bug from TransformStream

* fix(executor): skip content persistence when stream consumer exits early

Previously, if the onStream consumer caught an internal error without
re-throwing, the block-executor would treat the shortened accumulator
as the complete response, persist a truncated string to memory via
appendToMemory, and set it as executionOutput.content.

Track whether the source ReadableStream actually closed (done=true) in
the pull handler. If onStream returns before the source drains, skip
content persistence and log a warning — the old tee()-based flow was
immune to this because the executor branch drained independently of
the client branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix lint

* fix(executor): early-return when no streamed content to make onFullContent symmetric

Previously, executionOutput.content was guarded by `if (fullContent)`
but `onFullContent` fired regardless. The agent-handler implementor
defensively bails on empty/whitespace content, but that's a callee
contract, not enforced at the call site — future implementors could
spuriously persist empty assistant turns to memory.

Hoist the `!fullContent` check to a single early return, so both the
output write and the callback share the same precondition.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 14:23:36 -04:00
Waleed
c32c1cb917 fix(security): patch copilot tool & multipart upload IDORs (#4304)
* fix(security): patch copilot tool & multipart upload IDORs

- multipart upload: bind upload session to (userId, workspaceId, key)
  via short-lived HMAC-signed token; require workspace write access at
  initiate; source key/uploadId/context from verified token (never
  client) at get-part-urls/complete/abort
- copilot knowledge-base tools: gate all 11 read/write/tag/connector
  ops with checkKnowledgeBaseAccess / checkKnowledgeBaseWriteAccess
- copilot user-table tools: add workspace-id check to get, get_schema,
  add/rename/delete/update_column to match existing op pattern
- copilot manage-credential: add full ownership/write-permission auth
  via getCredentialActorContext (previously had no auth)
- copilot restore-resource: verify workspace ownership and write
  permission for workflow, table, knowledgebase, file, and folder
  restores
- copilot folder rename/move: verify both folderId and parentId belong
  to the caller's workspace via new verifyFolderWorkspace helper
- copilot get-job-logs: verify schedule belongs to caller's workspace

* fix(security): address PR review — document IDOR, log count, token split

- knowledge-base delete_document/update_document: verify each document
  belongs to the claimed knowledgeBaseId via checkDocumentWriteAccess
  (was: trusted args.knowledgeBaseId without binding it to the document)
- multipart batch complete: log verifiedEntries.length instead of raw
  client-supplied data.uploads.length
- upload-token: reject tokens with !=2 dot-delimited segments

* fix(security): close folder workspace bypass when workspaceId is falsy
2026-04-27 11:05:22 -07:00
Vikhyath Mondreti
50e74f75ef fix(mothership): parallel subagent rendering, exec stream re-attach (#4299)
* fix(mothership): parallel subagent rendering

* fix rendering logic

* address comments

* cleanup dead code

* address mothership legacy key

* prevent id collision

* rollout stream edge case

* address bugbot comments"

* remove unused fallbacks

* fix execution stream attach, cleanup comments

* remove debug logs

* cleanup failed reconnect
2026-04-27 00:40:39 -07:00
Waleed
60652e621c fix(security): credential-set invite email check + shopify authorize XSS (#4302) 2026-04-26 20:52:42 -07:00
Waleed
8863f1132a feat(models): add gpt-5.5 models (#4300)
* feat(models): add gpt-5.5 models

* fix(models): address gpt-5.5 review feedback

* fix(models): align gpt-5.5 pro controls
2026-04-25 19:13:29 -07:00
Vikhyath Mondreti
d93a6f57bc chore(guide): update contributing guide (#4296)
* chore(guide): update contributing guide

* fix md
2026-04-24 18:47:57 -07:00
Vikhyath Mondreti
df581c3efb fix(mothership): queue supersede crash (#4297)
* fix(mothership): queue supersede crash

* add test

* abort observed marker
2026-04-24 18:31:46 -07:00
Vikhyath Mondreti
f16d17ba49 improvement(mothership): do not silently re-route missing stream id (#4295) 2026-04-24 16:37:40 -07:00
Waleed
e6fefc863c fix(table-block): resolve canonical tableId in filter/sort builders (#4294)
The visual filter/sort builders read the selected tableId from subBlock id
'tableId', but the Table block stores it under 'tableSelector' (basic) or
'manualTableId' (advanced) via canonicalParamId. The lookup always returned
null, so useTable was disabled and the column picker always showed
"no options available".

Adds useCanonicalSubBlockValue that resolves by canonicalParamId through
the canonical index, mirroring the pattern used by dropdown dependsOn.
2026-04-24 16:24:49 -07:00
Waleed
5fba724818 chore(bun): bump bun to 1.3.13 (#4291) 2026-04-24 15:59:10 -07:00
Waleed
60b80ec172 improvement(tables): race-free row-count trigger + scoped tx timeouts (#4289)
* improvement(tables): race-free row-count trigger + scoped tx timeouts

* fix(tables): close upsert race + serialize replaceTableRows

Two concurrency bugs flagged by review:

1. `upsertRow` insert path: removing FOR UPDATE broke serialization between
   the initial existing-row SELECT and the INSERT. Two concurrent upserts
   on the same conflict target could both find no match, then both insert,
   producing a duplicate that bypasses the app-level unique check. Fixed
   by re-checking for the matching row *after* acquiring the per-table
   advisory lock; if a racing tx already inserted, switch to UPDATE.

2. `replaceTableRows`: under READ COMMITTED each tx's DELETE uses its own
   MVCC snapshot, so two concurrent replaces could each DELETE only the
   rows visible at their start, then both INSERT, leaving the table with
   the union of both row sets. Fixed by acquiring the per-table advisory
   lock at the start of the tx to serialize replaces against each other
   and against auto-position inserts.

Also updated a stale docstring on `upsertRow` that still referenced the
removed FOR UPDATE.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(tables): serialize explicit-position inserts with advisory lock

The `(table_id, position)` index is non-unique. Concurrent explicit-
position inserts at the same slot can both observe the slot empty, both
skip the shift, then each INSERT — producing duplicate `(table_id,
position)` rows.

Take the per-table advisory lock in the explicit-position branches of
`insertRow` and `batchInsertRows` (the auto-position branches already do
this). Updated the test that asserted the explicit path skipped the lock,
and added coverage for `batchInsertRows` with explicit positions.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(tables): dedupe upsert UPDATE path + extract nextAutoPosition

Two pure cleanups on the round-1 changes:

1. Extract `nextAutoPosition(trx, tableId)` — the `SELECT coalesce(max(
   position), -1) + 1` pattern was repeated in three call sites
   (`insertRow` auto branch, `batchInsertRows` auto branch, `upsertRow`
   insert branch). One helper, same behavior.

2. Consolidate `upsertRow` update path. The initial-SELECT match and the
   post-lock re-select match previously had two literal duplicates of the
   same UPDATE + return block. Resolve `matchedRowId` first, then run one
   UPDATE branch. Lock is still only acquired when we don't find a match
   on the first pass.

No behavior change. 98/98 table unit tests unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 15:33:17 -07:00
Vikhyath Mondreti
af4be770a1 improvement(mothership): treat error as terminal event (#4290) 2026-04-24 14:51:26 -07:00
Waleed
f330fe22a2 refactor(ashby): align tools, block, and triggers with Ashby API (#4288)
* refactor(ashby): align tools, block, and triggers with Ashby API

Audit-driven refactor to destructure rich fields per Ashby's API docs,
centralize output shapes via shared mappers in tools/ashby/utils.ts,
and align webhook provider handler with trigger IDs via a shared
action map. Removes stale block outputs left over from prior flat
response shapes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(ashby): remove stale noteId output and reject ping events

- Remove stale `noteId` output descriptor from block (create_note
  now returns `id` at the top level via the shared note mapper).
- Explicitly reject `ping` events in the webhook matchEvent before
  falling back to the generic triggerId check, so webhook records
  missing triggerId cannot execute workflows on Ashby ping probes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(ashby): trim optional ID params in create/update tools

Optional ID params in create_application, change_application_stage,
and update_candidate were passed through to the request body without
.trim(), unlike their required ID siblings. Normalize to prevent
copy-paste whitespace errors.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(ashby): add subblock migration for removed filterCandidateId

Add SUBBLOCK_ID_MIGRATIONS entry so deployed workflows that previously
used the `filterCandidateId` subBlock on `list_applications` don't break
after the field was removed (Ashby's application.list doesn't filter by
candidateId). Also regenerate docs to sync noteId removal.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(ashby): final API alignment from parallel validation

- create_candidate: email is optional per Ashby docs (only name is
  required); tool, types, and block all made non-required.
- list_applications: guard NaN when createdAfter can't be parsed so we
  don't send a bad value to Ashby's API.
- webhook provider: replace createHmacVerifier with explicit
  fail-closed verifyAuth that 401s when secretToken, signature header,
  or signature match is missing (was previously fail-open on missing
  secret).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(ashby): preserve input.data path in webhook formatInput

Restore the explicit `data` key alongside the spread so deployed
workflows that reference `input.data.application.*`, `input.data.candidate.*`,
etc. keep working. The spread alone dropped those paths.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(ashby): drop legacy input.data key from webhook formatInput

Keep formatInput aligned with the advertised trigger outputs schema
(flat top-level entities) and drop the legacy input.data.* compat path.
Every field declared in each trigger's outputs is now populated 1:1 by
the data spread plus the explicit action key — no undeclared keys.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(ashby): trim remaining ID body params for parity

Add .trim() on sourceId (create_candidate), jobId (list_applications),
applicationId and interviewStageId (list_interviews) to match the
trim-on-IDs pattern used across the rest of the Ashby tools and guard
against copy-paste whitespace.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* update docs

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 13:29:26 -07:00
d 🔹
efc868263a fix(copilot): replace crypto.randomUUID() with generateId() per project rule (#4268)
AGENTS.md / CLAUDE.md forbid crypto.randomUUID() (non-secure contexts throw
TypeError in browsers). Four copilot server-side files still violated this
rule, left over after PR #3397 polyfilled the client.

Routes through request lifecycle, OAuth draft insertion, persisted message
normalization, and table-row generation now use generateId from @sim/utils/id,
which is a drop-in UUID v4 producer that falls back to crypto.getRandomValues
when randomUUID is unavailable.

Refs #3393.
2026-04-24 11:59:48 -07:00
Vikhyath Mondreti
56044776d5 improvement(mothership): stream retry state machine, progressive re-rendering (#4287)
* improvement(mothership): stream rety state machine, progressive re-rendering

* address comments
2026-04-24 11:56:53 -07:00
Theodore Li
04f1d015f3 fix(mothership): Use heartbeat mechanism for chat locks (#4286) 2026-04-24 14:36:50 -04:00
Waleed
ccb5f1e690 fix(db): revert statement_timeout startup options breaking pooled connections (#4284) 2026-04-23 23:31:52 -07:00
Waleed
91ccbb9921 fix(agentphone): fix image (#4281) 2026-04-23 16:54:32 -07:00
Theodore Li
dcbe7c69b0 feat(ui): Show subagent logs in bounded vertical view (#4280)
* feat(ui): render subagent actions in bounded box.

* Add gradients and scroll bar

* fix lint
2026-04-23 18:41:53 -04:00
Theodore Li
c22ac38ab0 Set statement timeout of 90 seconds (#4276) 2026-04-23 18:27:15 -04:00
Waleed
cdde8cbd66 feat(agentphone): add AgentPhone integration (#4278)
* feat(agentphone): add AgentPhone integration

* fix(agentphone): validate numeric inputs and metadata JSON

* chore(agentphone): remove dead from fallback in get_number_messages

* fix(agentphone): drop empty-string updates in update_contact

* fix(agentphone): scope limit/offset to list ops and revert stray IdentityCenter change

* lint
2026-04-23 15:20:19 -07:00
Waleed
0ae19dab85 feat(files): default sort by updated and add updated sort option (#4279)
* feat(files): default sort by updated and add updated sort option

* feat(files): show Last Updated column

Matches the visible-column pattern already used on Knowledge and Tables
so users can see the value they're sorting by.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 15:02:07 -07:00
Waleed
3b11c814f8 fix(tables): account for letter-spacing and displayed content in column auto-resize (#4277) 2026-04-23 14:41:40 -07:00
Theodore Li
b86ebb35fd fix(api): Pass archivedAt to list table response (#4275)
* fix(retention): switch data retention to be org-level

* fix lint

* cleanup mothership ran logs

* fix cleanup dispatcher

* fix ui flash for data retention settings

* fix lint

* remove raw sql string interprolation

* fix(api): return archivedAt for list tables route
2026-04-23 14:32:37 -04:00
Theodore Li
65972f2fa3 fix(retention): switch data retention to be org-level (#4270)
* fix(retention): switch data retention to be org-level

* fix lint

* cleanup mothership ran logs

* fix cleanup dispatcher

* fix ui flash for data retention settings

* fix lint

* remove raw sql string interprolation
2026-04-23 02:41:49 -04:00
Vikhyath Mondreti
5f0f0edd63 improvement(repo): separate realtime into separate app (#4262)
* improvement(repo): restructuring to make realtime image narrower scoped

* improvements

* chore(repo): rebase fixes and quality improvements for realtime split

Addresses merge-time issues and gaps from the realtime app split:
- Retarget stale vi.mock paths to @sim/workflow-persistence/subblocks
- Restore README branding, fix AGENTS.md script reference
- Restore TSDoc on workflow-persistence subblocks helpers
- Use toError() from @sim/utils/errors in save.ts
- Add vitest config + local mocks so @sim/audit tests run standalone
- Move socket.io-client to devDependencies in apps/realtime
- Add missing package COPY steps to docker/app.Dockerfile
- Add check:boundaries/check:realtime-prune scripts and wire into CI

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(security): consolidate crypto primitives into @sim/security

Move general-purpose crypto primitives out of apps/sim into the
@sim/security package so both apps/sim and apps/realtime can share them.

@sim/security exports (all pure, dependency-free):
  ./compare    safeCompare (constant-time HMAC-wrapped equality)
  ./encryption encrypt/decrypt (AES-256-GCM, iv:cipher:tag format)
  ./hash       sha256Hex
  ./tokens     generateSecureToken (base64url)

Migrate apps/sim call sites to use these + @sim/utils helpers:
  crypto.randomUUID()            -> generateId() from @sim/utils/id
  createHash('sha256').digest    -> sha256Hex
  timingSafeEqual on hashed hex  -> safeCompare
  new Promise(setTimeout)        -> sleep from @sim/utils/helpers

No behavior change: encryption format, digest output, and token
length are preserved exactly.

* refactor(copilot): use toError in remaining otel/finalize sites

Replace the last two `error instanceof Error ? error : new Error(String(error))`
patterns with toError from @sim/utils/errors. Completes the sweep of clean
candidates — no behavior change.

* refactor(security): consolidate HMAC-SHA256 primitives into @sim/security

Adds hmacSha256Hex and hmacSha256Base64 to @sim/security/hmac and migrates
15 webhook providers plus 5 other hot paths (deployment token signing,
outbound webhook requests, workspace notification delivery, notification
test route, Shopify OAuth callback) off bare `createHmac` calls. Secret
parameter accepts `string | Buffer` to cover base64-decoded Svix-style
secrets (Resend) and MS Teams' HMAC scheme. AWS SigV4 signing in S3 and
Textract tools intentionally retains direct `createHmac` usage — its
multi-step key derivation chain doesn't fit a generic helper.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore(packages): post-audit test + packaging polish

- Add safeCompare unit tests (identity, length mismatch, hex-nibble diff).
- Add Buffer-secret cases to hmac tests to lock in Svix/MS-Teams contract.
- Declare `reactflow` as a peerDependency on @sim/workflow-types — only used for type imports.
- Add a barrel export to @sim/workflow-persistence for consumers that prefer package-level imports; subpath exports retained.
- Document the data-field invariant in load.ts for loop/parallel subflow patching.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore(realtime): address PR review feedback

- Remove redundant SOCKET_PORT=3002 env from Dockerfile runner stage
  (env.PORT already defaults to 3002 via zod schema).
- Reorder PORT fallback so an explicitly-set SOCKET_PORT wins over
  the schema default for PORT; keeps SOCKET_PORT functional as an
  override instead of dead code.
- Add dedicated type-check CI step for @sim/realtime so TS errors
  surface pre-deploy (the Dockerfile runs source TS via Bun and has
  no implicit build-time type check).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore(realtime): remove unused SOCKET_PORT env var

SOCKET_PORT has lived in the socket server since the June 2025 refactor
but was never actually set in any deploy config — docker-compose.prod,
helm values/templates, .env.example, and docs all use PORT or the 3002
default exclusively. No self-hoster was ever pointed at SOCKET_PORT, so
removing it is safe.

Simplifies realtime port resolution to `env.PORT` (zod-validated with a
3002 default) and drops the orphaned sim-side schema entry.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Waleed Latif <walif6@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 23:06:16 -07:00
Waleed
bed5e95742 fix(selectors): enable search on all picker and selector subBlocks (#4269) 2026-04-22 20:40:59 -07:00
Theodore Li
f7ab39984c feat(ui): add thinking ui to mothership (#4254)
* feat(ui): Add thinking ui

* fix tests

* Remove duplicate helper for block timing

* fix lint

* fix endedAt timestamp bug

* fix stuck subagent thinking
2026-04-22 19:52:56 -04:00
Theodore Li
8ce56fe1f2 fix(auth): add api key auth via sha256 hash lookup (#4266)
* fix(auth): add api key auth via sha256 hash lookup

* Remove promise all logic

* Restore feature flag

* fix feature flag

* Combine auth and hash gate
2026-04-22 18:30:37 -04:00
649 changed files with 104260 additions and 5284 deletions

View File

@@ -144,7 +144,7 @@ vi.useFakeTimers()
| `@/app/api/auth/oauth/utils` | `authOAuthUtilsMock`, `authOAuthUtilsMockFns` | `vi.mock('@/app/api/auth/oauth/utils', () => authOAuthUtilsMock)` |
| `@/app/api/knowledge/utils` | `knowledgeApiUtilsMock`, `knowledgeApiUtilsMockFns` | `vi.mock('@/app/api/knowledge/utils', () => knowledgeApiUtilsMock)` |
| `@/app/api/workflows/utils` | `workflowsApiUtilsMock`, `workflowsApiUtilsMockFns` | `vi.mock('@/app/api/workflows/utils', () => workflowsApiUtilsMock)` |
| `@/lib/audit/log` | `auditMock`, `auditMockFns` | `vi.mock('@/lib/audit/log', () => auditMock)` |
| `@sim/audit` | `auditMock`, `auditMockFns` | `vi.mock('@sim/audit', () => auditMock)` |
| `@/lib/auth` | `authMock`, `authMockFns` | `vi.mock('@/lib/auth', () => authMock)` |
| `@/lib/auth/hybrid` | `hybridAuthMock`, `hybridAuthMockFns` | `vi.mock('@/lib/auth/hybrid', () => hybridAuthMock)` |
| `@/lib/copilot/request/http` | `copilotHttpMock`, `copilotHttpMockFns` | `vi.mock('@/lib/copilot/request/http', () => copilotHttpMock)` |

View File

@@ -144,7 +144,7 @@ vi.useFakeTimers()
| `@/app/api/auth/oauth/utils` | `authOAuthUtilsMock`, `authOAuthUtilsMockFns` | `vi.mock('@/app/api/auth/oauth/utils', () => authOAuthUtilsMock)` |
| `@/app/api/knowledge/utils` | `knowledgeApiUtilsMock`, `knowledgeApiUtilsMockFns` | `vi.mock('@/app/api/knowledge/utils', () => knowledgeApiUtilsMock)` |
| `@/app/api/workflows/utils` | `workflowsApiUtilsMock`, `workflowsApiUtilsMockFns` | `vi.mock('@/app/api/workflows/utils', () => workflowsApiUtilsMock)` |
| `@/lib/audit/log` | `auditMock`, `auditMockFns` | `vi.mock('@/lib/audit/log', () => auditMock)` |
| `@sim/audit` | `auditMock`, `auditMockFns` | `vi.mock('@sim/audit', () => auditMock)` |
| `@/lib/auth` | `authMock`, `authMockFns` | `vi.mock('@/lib/auth', () => authMock)` |
| `@/lib/auth/hybrid` | `hybridAuthMock`, `hybridAuthMockFns` | `vi.mock('@/lib/auth/hybrid', () => hybridAuthMock)` |
| `@/lib/copilot/request/http` | `copilotHttpMock`, `copilotHttpMockFns` | `vi.mock('@/lib/copilot/request/http', () => copilotHttpMock)` |

View File

@@ -1,4 +1,4 @@
FROM oven/bun:1.3.11-alpine
FROM oven/bun:1.3.13-alpine
# Install necessary packages for development
RUN apk add --no-cache \

View File

@@ -71,7 +71,7 @@ fi
# Set up environment variables if .env doesn't exist for the sim app
if [ ! -f "apps/sim/.env" ]; then
echo "📄 Creating .env file from template..."
echo "📄 Creating apps/sim/.env from template..."
if [ -f "apps/sim/.env.example" ]; then
cp apps/sim/.env.example apps/sim/.env
else
@@ -79,6 +79,18 @@ if [ ! -f "apps/sim/.env" ]; then
fi
fi
# Set up env for the realtime server (must match the shared values in apps/sim/.env)
if [ ! -f "apps/realtime/.env" ] && [ -f "apps/realtime/.env.example" ]; then
echo "📄 Creating apps/realtime/.env from template..."
cp apps/realtime/.env.example apps/realtime/.env
fi
# Set up packages/db/.env for drizzle-kit and migration scripts
if [ ! -f "packages/db/.env" ] && [ -f "packages/db/.env.example" ]; then
echo "📄 Creating packages/db/.env from template..."
cp packages/db/.env.example packages/db/.env
fi
# Generate schema and run database migrations
echo "🗃️ Running database schema generation and migrations..."
echo "Generating schema..."

View File

@@ -2,8 +2,15 @@
Thank you for your interest in contributing to Sim! Our goal is to provide developers with a powerful, user-friendly platform for building, testing, and optimizing agentic workflows. We welcome contributions in all forms—from bug fixes and design improvements to brand-new features.
> **Project Overview:**
> Sim is a monorepo using Turborepo, containing the main application (`apps/sim/`), documentation (`apps/docs/`), and shared packages (`packages/`). The main application is built with Next.js (app router), ReactFlow, Zustand, Shadcn, and Tailwind CSS. Please ensure your contributions follow our best practices for clarity, maintainability, and consistency.
> **Project Overview:**
> Sim is a Turborepo monorepo with two deployable apps and a set of shared packages:
>
> - `apps/sim/` — the main Next.js application (App Router, ReactFlow, Zustand, Shadcn, Tailwind CSS).
> - `apps/realtime/` — a small Bun + Socket.IO server that powers the collaborative canvas. Shares DB and Better Auth secrets with `apps/sim` via `@sim/*` packages.
> - `apps/docs/` — Fumadocs-based documentation site.
> - `packages/` — shared workspace packages (`@sim/db`, `@sim/auth`, `@sim/audit`, `@sim/workflow-types`, `@sim/workflow-persistence`, `@sim/workflow-authz`, `@sim/realtime-protocol`, `@sim/security`, `@sim/logger`, `@sim/utils`, `@sim/testing`, `@sim/tsconfig`).
>
> Strict one-way dependency flow: `apps/* → packages/*`. Packages never import from apps. Please ensure your contributions follow this and our best practices for clarity, maintainability, and consistency.
---
@@ -24,14 +31,17 @@ Thank you for your interest in contributing to Sim! Our goal is to provide devel
We strive to keep our workflow as simple as possible. To contribute:
1. **Fork the Repository**
1. **Fork the Repository**
Click the **Fork** button on GitHub to create your own copy of the project.
2. **Clone Your Fork**
```bash
git clone https://github.com/<your-username>/sim.git
cd sim
```
3. **Create a Feature Branch**
3. **Create a Feature Branch**
Create a new branch with a descriptive name:
```bash
@@ -40,21 +50,23 @@ We strive to keep our workflow as simple as possible. To contribute:
Use a clear naming convention to indicate the type of work (e.g., `feat/`, `fix/`, `docs/`).
4. **Make Your Changes**
4. **Make Your Changes**
Ensure your changes are small, focused, and adhere to our coding guidelines.
5. **Commit Your Changes**
5. **Commit Your Changes**
Write clear, descriptive commit messages that follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/#specification) specification. This allows us to maintain a coherent project history and generate changelogs automatically. For example:
- `feat(api): add new endpoint for user authentication`
- `fix(ui): resolve button alignment issue`
- `docs: update contribution guidelines`
6. **Push Your Branch**
```bash
git push origin feat/your-feature-name
```
7. **Create a Pull Request**
7. **Create a Pull Request**
Open a pull request against the `staging` branch on GitHub. Please provide a clear description of the changes and reference any relevant issues (e.g., `fixes #123`).
---
@@ -65,7 +77,7 @@ If you discover a bug or have a feature request, please open an issue in our Git
- Provide a clear, descriptive title.
- Include as many details as possible (steps to reproduce, screenshots, etc.).
- **Tag Your Issue Appropriately:**
- **Tag Your Issue Appropriately:**
Use the following labels to help us categorize your issue:
- **active:** Actively working on it right now.
- **bug:** Something isn't working.
@@ -82,12 +94,11 @@ If you discover a bug or have a feature request, please open an issue in our Git
Before creating a pull request:
- **Ensure Your Branch Is Up-to-Date:**
- **Ensure Your Branch Is Up-to-Date:**
Rebase your branch onto the latest `staging` branch to prevent merge conflicts.
- **Follow the Guidelines:**
- **Follow the Guidelines:**
Make sure your changes are well-tested, follow our coding standards, and include relevant documentation if necessary.
- **Reference Issues:**
- **Reference Issues:**
If your PR addresses an existing issue, include `refs #<issue-number>` or `fixes #<issue-number>` in your PR description.
Our maintainers will review your pull request and provide feedback. We aim to make the review process as smooth and timely as possible.
@@ -166,27 +177,27 @@ To use local models with Sim:
1. Install Ollama and pull models:
```bash
# Install Ollama (if not already installed)
curl -fsSL https://ollama.ai/install.sh | sh
```bash
# Install Ollama (if not already installed)
curl -fsSL https://ollama.ai/install.sh | sh
# Pull a model (e.g., gemma3:4b)
ollama pull gemma3:4b
```
# Pull a model (e.g., gemma3:4b)
ollama pull gemma3:4b
```
2. Start Sim with local model support:
```bash
# With NVIDIA GPU support
docker compose --profile local-gpu -f docker-compose.ollama.yml up -d
```bash
# With NVIDIA GPU support
docker compose --profile local-gpu -f docker-compose.ollama.yml up -d
# Without GPU (CPU only)
docker compose --profile local-cpu -f docker-compose.ollama.yml up -d
# Without GPU (CPU only)
docker compose --profile local-cpu -f docker-compose.ollama.yml up -d
# If hosting on a server, update the environment variables in the docker-compose.prod.yml file
# to include the server's public IP then start again (OLLAMA_URL to i.e. http://1.1.1.1:11434)
docker compose -f docker-compose.prod.yml up -d
```
# If hosting on a server, update the environment variables in the docker-compose.prod.yml file
# to include the server's public IP then start again (OLLAMA_URL to i.e. http://1.1.1.1:11434)
docker compose -f docker-compose.prod.yml up -d
```
### Option 3: Using VS Code / Cursor Dev Containers
@@ -201,61 +212,104 @@ Dev Containers provide a consistent and easy-to-use development environment:
2. **Setup Steps:**
- Clone the repository:
```bash
git clone https://github.com/<your-username>/sim.git
cd sim
```
- Open the project in VS Code/Cursor
- When prompted, click "Reopen in Container" (or press F1 and select "Remote-Containers: Reopen in Container")
- Wait for the container to build and initialize
- Open the project in VS Code/Cursor.
- When prompted, click "Reopen in Container" (or press F1 and select "Remote-Containers: Reopen in Container").
- Wait for the container to build and initialize.
3. **Start Developing:**
- Run `bun run dev:full` in the terminal or use the `sim-start` alias
- This starts both the main application and the realtime socket server
- All dependencies and configurations are automatically set up
- Your changes will be automatically hot-reloaded
- Run `bun run dev:full` in the terminal or use the `sim-start` alias.
- This starts both the main application and the realtime socket server.
- All dependencies and configurations are automatically set up.
- Your changes will be automatically hot-reloaded.
4. **GitHub Codespaces:**
- This setup also works with GitHub Codespaces if you prefer development in the browser
- Just click "Code" → "Codespaces" → "Create codespace on staging"
- This setup also works with GitHub Codespaces if you prefer development in the browser.
- Just click "Code" → "Codespaces" → "Create codespace on staging".
### Option 4: Manual Setup
If you prefer not to use Docker or Dev Containers:
If you prefer not to use Docker or Dev Containers. **All commands run from the repository root unless explicitly noted.**
1. **Clone and Install:**
1. **Clone the Repository:**
```bash
git clone https://github.com/<your-username>/sim.git
cd sim
bun install
```
2. **Set Up Environment:**
Bun workspaces handle dependency resolution for all apps and packages from the root `bun install`.
- Navigate to the app directory:
```bash
cd apps/sim
```
- Copy `.env.example` to `.env`
- Configure required variables (DATABASE_URL, BETTER_AUTH_SECRET, BETTER_AUTH_URL)
2. **Set Up Environment Files:**
3. **Set Up Database:**
We use **per-app `.env` files** (the Turborepo-canonical pattern), not a single root `.env`. Three files are needed for local dev:
```bash
bunx drizzle-kit push
# Main app — large, app-specific (OAuth secrets, LLM keys, Stripe, etc.)
cp apps/sim/.env.example apps/sim/.env
# Realtime server — small, only the values shared with the main app
cp apps/realtime/.env.example apps/realtime/.env
# DB tooling (drizzle-kit, db:migrate)
cp packages/db/.env.example packages/db/.env
```
4. **Run the Development Server:**
At minimum, each `.env` needs `DATABASE_URL`. `apps/sim/.env` and `apps/realtime/.env` additionally need matching values for `BETTER_AUTH_URL`, `BETTER_AUTH_SECRET`, `INTERNAL_API_SECRET`, and `NEXT_PUBLIC_APP_URL`. `apps/sim/.env` also needs `ENCRYPTION_KEY` and `API_ENCRYPTION_KEY`. Generate any 32-char secrets with `openssl rand -hex 32`.
The same `BETTER_AUTH_SECRET`, `INTERNAL_API_SECRET`, and `DATABASE_URL` must appear in both `apps/sim/.env` and `apps/realtime/.env` so the two services share auth and DB. After editing `apps/sim/.env`, you can mirror the shared subset into the realtime env in one shot:
```bash
grep -E '^(DATABASE_URL|BETTER_AUTH_URL|BETTER_AUTH_SECRET|INTERNAL_API_SECRET|NEXT_PUBLIC_APP_URL|REDIS_URL)=' apps/sim/.env > apps/realtime/.env
grep -E '^DATABASE_URL=' apps/sim/.env > packages/db/.env
```
3. **Run Database Migrations:**
Migrations live in `packages/db/migrations/`. Run them via the dedicated workspace script:
```bash
cd packages/db && bun run db:migrate && cd ../..
```
For ad-hoc schema iteration during development you can also use `bun run db:push` from `packages/db`, but `db:migrate` is the canonical command for both local and CI/CD setups.
4. **Run the Development Servers:**
```bash
bun run dev:full
```
This command starts both the main application and the realtime socket server required for full functionality.
This launches both apps with coloured prefixes:
- `[App]` — Next.js on `http://localhost:3000`
- `[Realtime]` — Socket.IO on `http://localhost:3002`
Or run them separately:
```bash
bun run dev # Next.js app only
bun run dev:sockets # realtime server only
```
5. **Make Your Changes and Test Locally.**
Before opening a PR, run the same checks CI runs:
```bash
bun run type-check # TypeScript across every workspace
bun run lint:check # Biome lint across every workspace
bun run test # Vitest across every workspace
```
### Email Template Development
When working on email templates, you can preview them using a local email preview server:
@@ -263,18 +317,19 @@ When working on email templates, you can preview them using a local email previe
1. **Run the Email Preview Server:**
```bash
bun run email:dev
cd apps/sim && bun run email:dev
```
2. **Access the Preview:**
- Open `http://localhost:3000` in your browser
- You'll see a list of all email templates
- Click on any template to view and test it with various parameters
- Open `http://localhost:3000` in your browser.
- You'll see a list of all email templates.
- Click on any template to view and test it with various parameters.
3. **Templates Location:**
- Email templates are located in `sim/app/emails/`
- After making changes to templates, they will automatically update in the preview
- Email templates live in `apps/sim/components/emails/`.
- Changes hot-reload automatically in the preview.
---
@@ -282,28 +337,41 @@ When working on email templates, you can preview them using a local email previe
Sim is built in a modular fashion where blocks and tools extend the platform's functionality. To maintain consistency and quality, please follow the guidelines below when adding a new block or tool.
> **Use the skill guides for step-by-step recipes.** The repository ships opinionated, end-to-end guides under `.agents/skills/` that cover the exact file layout, conventions, registry wiring, and gotchas for each kind of contribution. Read the relevant SKILL.md before you start writing code:
>
> | Adding… | Read |
> | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
> | A new integration end-to-end (tools + block + icon + optional triggers + all registrations) | [`.agents/skills/add-integration/SKILL.md`](../.agents/skills/add-integration/SKILL.md) |
> | Just a block (or aligning an existing block with its tools) | [`.agents/skills/add-block/SKILL.md`](../.agents/skills/add-block/SKILL.md) |
> | Just tool configs for a service | [`.agents/skills/add-tools/SKILL.md`](../.agents/skills/add-tools/SKILL.md) |
> | A webhook trigger for a service | [`.agents/skills/add-trigger/SKILL.md`](../.agents/skills/add-trigger/SKILL.md) |
> | A knowledge-base connector (sync docs from an external source) | [`.agents/skills/add-connector/SKILL.md`](../.agents/skills/add-connector/SKILL.md) |
>
> The shorter overview below is a high-level reference; the SKILL.md files are the authoritative source of truth and stay in sync with the codebase.
### Where to Add Your Code
- **Blocks:** Create your new block file under the `/apps/sim/blocks/blocks` directory. The name of the file should match the provider name (e.g., `pinecone.ts`).
- **Tools:** Create a new directory under `/apps/sim/tools` with the same name as the provider (e.g., `/apps/sim/tools/pinecone`).
- **Blocks:** Create your new block file under the `apps/sim/blocks/blocks/` directory. The name of the file should match the provider name (e.g., `pinecone.ts`).
- **Tools:** Create a new directory under `apps/sim/tools/` with the same name as the provider (e.g., `apps/sim/tools/pinecone`).
In addition, you will need to update the registries:
- **Block Registry:** Update the blocks index (`/apps/sim/blocks/index.ts`) to include your new block.
- **Tool Registry:** Update the tools registry (`/apps/sim/tools/index.ts`) to add your new tool.
- **Block Registry:** Add your block to `apps/sim/blocks/registry.ts`. (`apps/sim/blocks/index.ts` re-exports lookups from the registry; you do not need to edit it.)
- **Tool Registry:** Add your tool to `apps/sim/tools/index.ts`.
### How to Create a New Block
1. **Create a New File:**
Create a file for your block named after the provider (e.g., `pinecone.ts`) in the `/apps/sim/blocks/blocks` directory.
1. **Create a New File:**
Create a file for your block named after the provider (e.g., `pinecone.ts`) in the `apps/sim/blocks/blocks/` directory.
2. **Create a New Icon:**
Create a new icon for your block in the `/apps/sim/components/icons.tsx` file. The icon should follow the same naming convention as the block (e.g., `PineconeIcon`).
Create a new icon for your block in `apps/sim/components/icons.tsx`. The icon should follow the same naming convention as the block (e.g., `PineconeIcon`).
3. **Define the Block Configuration:**
3. **Define the Block Configuration:**
Your block should export a constant of type `BlockConfig`. For example:
```typescript:/apps/sim/blocks/blocks/pinecone.ts
```typescript
// apps/sim/blocks/blocks/pinecone.ts
import { PineconeIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import type { PineconeResponse } from '@/tools/pinecone/types'
@@ -321,7 +389,7 @@ In addition, you will need to update the registries:
{
id: 'operation',
title: 'Operation',
type: 'dropdown'
type: 'dropdown',
required: true,
options: [
{ label: 'Generate Embeddings', id: 'generate' },
@@ -332,7 +400,7 @@ In addition, you will need to update the registries:
{
id: 'apiKey',
title: 'API Key',
type: 'short-input'
type: 'short-input',
placeholder: 'Your Pinecone API key',
password: true,
required: true,
@@ -370,10 +438,11 @@ In addition, you will need to update the registries:
}
```
4. **Register Your Block:**
Add your block to the blocks registry (`/apps/sim/blocks/registry.ts`):
4. **Register Your Block:**
Add your block to the blocks registry (`apps/sim/blocks/registry.ts`):
```typescript:/apps/sim/blocks/registry.ts
```typescript
// apps/sim/blocks/registry.ts
import { PineconeBlock } from '@/blocks/blocks/pinecone'
// Registry of all available blocks
@@ -385,24 +454,25 @@ In addition, you will need to update the registries:
The block will be automatically available to the application through the registry.
5. **Test Your Block:**
5. **Test Your Block:**
Ensure that the block displays correctly in the UI and that its functionality works as expected.
### How to Create a New Tool
1. **Create a New Directory:**
Create a directory under `/apps/sim/tools` with the same name as the provider (e.g., `/apps/sim/tools/pinecone`).
1. **Create a New Directory:**
Create a directory under `apps/sim/tools/` with the same name as the provider (e.g., `apps/sim/tools/pinecone`).
2. **Create Tool Files:**
2. **Create Tool Files:**
Create separate files for each tool functionality with descriptive names (e.g., `fetch.ts`, `generate_embeddings.ts`, `search_text.ts`) in your tool directory.
3. **Create a Types File:**
3. **Create a Types File:**
Create a `types.ts` file in your tool directory to define and export all types related to your tools.
4. **Create an Index File:**
4. **Create an Index File:**
Create an `index.ts` file in your tool directory that imports and exports all tools:
```typescript:/apps/sim/tools/pinecone/index.ts
```typescript
// apps/sim/tools/pinecone/index.ts
import { fetchTool } from './fetch'
import { generateEmbeddingsTool } from './generate_embeddings'
import { searchTextTool } from './search_text'
@@ -410,10 +480,11 @@ In addition, you will need to update the registries:
export { fetchTool, generateEmbeddingsTool, searchTextTool }
```
5. **Define the Tool Configuration:**
5. **Define the Tool Configuration:**
Your tool should export a constant with a naming convention of `{toolName}Tool`. The tool ID should follow the format `{provider}_{tool_name}`. For example:
```typescript:/apps/sim/tools/pinecone/fetch.ts
```typescript
// apps/sim/tools/pinecone/fetch.ts
import { ToolConfig, ToolResponse } from '@/tools/types'
import { PineconeParams, PineconeResponse } from '@/tools/pinecone/types'
@@ -449,11 +520,12 @@ In addition, you will need to update the registries:
}
```
6. **Register Your Tool:**
Update the tools registry in `/apps/sim/tools/index.ts` to include your new tool:
6. **Register Your Tool:**
Update the tools registry in `apps/sim/tools/index.ts` to include your new tool:
```typescript:/apps/sim/tools/index.ts
import { fetchTool, generateEmbeddingsTool, searchTextTool } from '/@tools/pinecone'
```typescript
// apps/sim/tools/index.ts
import { fetchTool, generateEmbeddingsTool, searchTextTool } from '@/tools/pinecone'
// ... other imports
export const tools: Record<string, ToolConfig> = {
@@ -464,13 +536,14 @@ In addition, you will need to update the registries:
}
```
7. **Test Your Tool:**
7. **Test Your Tool:**
Ensure that your tool functions correctly by making test requests and verifying the responses.
8. **Generate Documentation:**
Run the documentation generator to create docs for your new tool:
8. **Generate Documentation:**
Run the documentation generator (from `apps/sim`) to create docs for your new tool:
```bash
./scripts/generate-docs.sh
cd apps/sim && bun run generate-docs
```
### Naming Conventions
@@ -480,7 +553,7 @@ Maintaining consistent naming across the codebase is critical for auto-generatio
- **Block Files:** Name should match the provider (e.g., `pinecone.ts`)
- **Block Export:** Should be named `{Provider}Block` (e.g., `PineconeBlock`)
- **Icons:** Should be named `{Provider}Icon` (e.g., `PineconeIcon`)
- **Tool Directories:** Should match the provider name (e.g., `/tools/pinecone/`)
- **Tool Directories:** Should match the provider name (e.g., `tools/pinecone/`)
- **Tool Files:** Should be named after their function (e.g., `fetch.ts`, `search_text.ts`)
- **Tool Exports:** Should be named `{toolName}Tool` (e.g., `fetchTool`)
- **Tool IDs:** Should follow the format `{provider}_{tool_name}` (e.g., `pinecone_fetch`)
@@ -489,12 +562,12 @@ Maintaining consistent naming across the codebase is critical for auto-generatio
Sim implements a sophisticated parameter visibility system that controls how parameters are exposed to users and LLMs in agent workflows. Each parameter can have one of four visibility levels:
| Visibility | User Sees | LLM Sees | How It Gets Set |
|-------------|-----------|----------|--------------------------------|
| `user-only` | ✅ Yes | ❌ No | User provides in UI |
| `user-or-llm` | ✅ Yes | ✅ Yes | User provides OR LLM generates |
| `llm-only` | ❌ No | ✅ Yes | LLM generates only |
| `hidden` | ❌ No | ❌ No | Application injects at runtime |
| Visibility | User Sees | LLM Sees | How It Gets Set |
| ------------- | --------- | -------- | ------------------------------ |
| `user-only` | ✅ Yes | ❌ No | User provides in UI |
| `user-or-llm` | ✅ Yes | ✅ Yes | User provides OR LLM generates |
| `llm-only` | ❌ No | ✅ Yes | LLM generates only |
| `hidden` | ❌ No | ❌ No | Application injects at runtime |
#### Visibility Guidelines

View File

@@ -20,7 +20,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.11
bun-version: 1.3.13
- name: Setup Node
uses: actions/setup-node@v4

View File

@@ -23,7 +23,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.11
bun-version: 1.3.13
- name: Cache Bun dependencies
uses: actions/cache@v4
@@ -122,7 +122,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.11
bun-version: 1.3.13
- name: Cache Bun dependencies
uses: actions/cache@v4

View File

@@ -19,7 +19,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.11
bun-version: 1.3.13
- name: Cache Bun dependencies
uses: actions/cache@v4

View File

@@ -19,7 +19,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.11
bun-version: 1.3.13
- name: Setup Node.js for npm publishing
uses: actions/setup-node@v4

View File

@@ -19,7 +19,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.11
bun-version: 1.3.13
- name: Setup Node.js for npm publishing
uses: actions/setup-node@v4

View File

@@ -19,7 +19,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.11
bun-version: 1.3.13
- name: Setup Node
uses: actions/setup-node@v4
@@ -103,6 +103,15 @@ jobs:
- name: Lint code
run: bun run lint:check
- name: Enforce monorepo boundaries
run: bun run check:boundaries
- name: Verify realtime prune graph
run: bun run check:realtime-prune
- name: Type-check realtime server
run: bunx turbo run type-check --filter=@sim/realtime
- name: Run tests with coverage
env:
NODE_OPTIONS: '--no-warnings --max-old-space-size=8192'

View File

@@ -20,19 +20,42 @@ You are a professional software engineer. All code must follow best practices: a
### Root Structure
```
apps/sim/
├── app/ # Next.js app router (pages, API routes)
├── blocks/ # Block definitions and registry
├── components/ # Shared UI (emcn/, ui/)
├── executor/ # Workflow execution engine
├── hooks/ # Shared hooks (queries/, selectors/)
├── lib/ # App-wide utilities
├── providers/ # LLM provider integrations
├── stores/ # Zustand stores
├── tools/ # Tool definitions
└── triggers/ # Trigger definitions
apps/
├── sim/ # Next.js app (UI + API routes + workflow editor)
│ ├── app/ # Next.js app router (pages, API routes)
│ ├── blocks/ # Block definitions and registry
│ ├── components/ # Shared UI (emcn/, ui/)
│ ├── executor/ # Workflow execution engine
│ ├── hooks/ # Shared hooks (queries/, selectors/)
│ ├── lib/ # App-wide utilities
│ ├── providers/ # LLM provider integrations
├── stores/ # Zustand stores
│ ├── tools/ # Tool definitions
│ └── triggers/ # Trigger definitions
└── realtime/ # Bun Socket.IO server (collaborative canvas)
└── src/ # auth, config, database, handlers, middleware,
# rooms, routes, internal/webhook-cleanup.ts
packages/
├── audit/ # @sim/audit — recordAudit + AuditAction + AuditResourceType
├── auth/ # @sim/auth — @sim/auth/verify (shared Better Auth verifier)
├── db/ # @sim/db — drizzle schema + client
├── logger/ # @sim/logger
├── realtime-protocol/ # @sim/realtime-protocol — socket operation constants + zod schemas
├── security/ # @sim/security — safeCompare
├── tsconfig/ # shared tsconfig presets
├── utils/ # @sim/utils
├── workflow-authz/ # @sim/workflow-authz — authorizeWorkflowByWorkspacePermission
├── workflow-persistence/ # @sim/workflow-persistence — raw load/save + subflow helpers
└── workflow-types/ # @sim/workflow-types — pure BlockState/Loop/Parallel/... types
```
### Package boundaries
- `apps/* → packages/*` only. Packages never import from `apps/*`.
- Each package has explicit subpath `exports` maps; no barrels that accidentally pull in heavy halves.
- `apps/realtime` intentionally avoids Next.js, React, the block/tool registry, provider SDKs, and the executor. CI enforces this via `scripts/check-monorepo-boundaries.ts` and `scripts/check-realtime-prune-graph.ts`.
- Auth is shared across services via the Better Auth "Shared Database Session" pattern: both apps read the same `BETTER_AUTH_SECRET` and point at the same DB via `@sim/db`.
### Naming Conventions
- Components: PascalCase (`WorkflowList`)
- Hooks: `use` prefix (`useWorkflowOperations`)

View File

@@ -28,6 +28,36 @@ export function AgentMailIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function AgentPhoneIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} viewBox='0 0 150 150' xmlns='http://www.w3.org/2000/svg'>
<path
fill='#23AF58'
stroke='#007F3F'
strokeWidth='0.15'
strokeMiterlimit='10'
d='m139.6 53.3c-1.4-2.3-4.9-3.3-7.6-4.8-2.7-1.3-4.2-2.4-5.7-3.6-1.9-1-2.5-2.7-3.3-3.2s-2.7-1.4-4.5 1.3c-2 2.7-4.5 6.6-6.6 11.1-2.3 5.4-6.3 14.9-6.3 18.9 0.5 4.9 3.1 4.6 6.1 7.2 2.5 2.1 2.8 5.8 1.5 12.5-1.3 6.6-4 12.8-7.8 19.2-3.3 5.1-5.8 8.7-10 9.1-5.3 0.5-12.5-3.1-16.8-5.6-1-0.6-2.5-0.9-3.8-0.2-1.3 0.5-2.2 1.6-3.2 3.3-1.5 2.5-4.6 7.7-5.8 12.2-0.5 3 0 6.4 2.9 9 1.4 1.2 2.8 2.5 4.4 3.4 5 2.8 9.6 4.5 16.5 4.9 5.3 0.2 9.3-1 13.4-3.1 2.4-1.3 6.6-4.2 9.6-7.3l1.1-1.2c2.8-3.1 8.8-10 11.6-14.5 2.3-3.5 4.8-7.4 6.9-12.3 2.9-6.7 4.4-14 5-17.9 1.2-7 2.4-17.5 3.4-31.1 0.1-4.3-0.3-6.1-1-7.3zm-4.5 6.7c-0.5 9.5-1.9 23.3-3.1 30.1-0.9 4.5-2.4 9.6-3.8 13.4-1.1 2.6-3.1 7-5.6 10.8-3.4 5.3-8.4 11.6-12 15.8-6.4 6.6-10.2 9.6-14.2 10.8-2.2 0.9-3.8 1.2-7 1.2-3.4-0.1-8-0.7-11.3-2.2-3-1.2-7-4-6.9-6.8 0.4-3.2 3.3-9.6 5.2-11.9 0.2-0.3 0.5-0.3 0.7-0.2 2.5 1.1 6 3.2 9.6 4.5 2.4 0.9 4.8 1.4 7.3 1.4 3.9 0 6.7-1.2 9.5-3.2 5.6-4.6 9-10.8 12.1-17.5 2-4.3 4.1-11.6 4.4-18.3 0.1-4.9-1.1-8.9-4.5-12.2-1.1-0.7-3-2.1-3-2.8 0-4.2 3.9-13 8.9-22.9 0.2-0.7 0.5-1 1.1-0.7 1.1 0.6 3 1.4 4.6 2.4 2.1 1 5.4 2.4 7.1 3.9 0.9 0.4 1 3 0.9 4.4z'
/>
<path
fill='#23AF58'
d='m104.7 27.8c-1.3-1.5-3.3-1.3-6.2-1.5l-1.9 0.2-7-0.2-31.5 0.2 1.5-9.3c2-1.1 5.1-3.5 5.8-6.3 1-2.8 0.2-5.9-2-7.4-2.3-1.9-5.8-2.4-9.3-0.8-1.6 1-4.7 3.4-5.4 6.9-0.8 4.1 2.4 6.7 4.7 7.9l-1.5 9.1-17.2 0.9c-12.3 1.1-16.3 1.2-20.6 4.3-2 1.3-3 4.5-3.4 9.8-0.6 11.3-0.7 18.7-0.6 28.3 0.4 11.2 0 36.6 3 39.8l-1.2 0.3c-3.8 0.6-4 6.2-0.5 6.6l15.5-1 69.7-7.6c2.5-0.4 4.3-0.9 4.6-4.3l3.7-71.5c0-1.9 0.2-3.6-0.2-4.4zm-49.6-17.3c0.3-2.2 2.4-3 3.3-2.8 0.7 0.4 1 1.8 0 2.8-1.5 2-3.3 1.7-3.3 0zm40 90.2c-4 1-5.5 1.5-11.5 2.4-7.7 1-19.7 2.1-31.2 3.4l-33.8 2.9c-0.7 0.2-1-0.4-1-1-0.6-6.5-1.2-20.5-1.5-39.5l0.3-23.3c0.6-7.5 0.7-8.7 4.6-9.7 5.1-0.9 7.4-1.4 14.9-1.8l19.5-0.5 41.1-0.5c1.4 0 1.9 0.4 1.9 1.5l-3.3 66.1z'
/>
<path
fill='#23AF58'
d='m38.9 52.4c-1.8 0-4 1.1-4.5 3.3-1 3.9 1 7.6 4.5 7.7 3.8 0 5-3.8 4.7-6.3-0.2-2-2-4.7-4.7-4.7z'
/>
<path
fill='#23AF58'
d='m73.5 53.9c-1.8 0-4.3 1.5-4.4 4.5-0.1 3.2 2 5.3 4.3 5.3 2.5 0 4.2-1.7 4.2-4.8 0-3.2-1.7-4.8-4.1-5z'
/>
<path
fill='#23AF58'
d='m72.1 77.1c-2.7 3.4-7.2 7.4-14.7 8.3-7.3 0.3-13.9-2.9-20-8.5-3.5-3.4-8 0-6.2 2.7 1.7 2.5 6.4 6.6 10.4 8.8 3.5 2 7.3 3.3 13.8 3.5 4.7 0 9.2-0.8 12.7-2.4 2.9-1.1 5-2.8 6-3.8 2.3-2.1 3.8-4.1 3.5-7.3-0.9-2.5-3.6-2.8-5.5-1.3z'
/>
</svg>
)
}
export function CrowdStrikeIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} viewBox='0 0 768 500' fill='none' xmlns='http://www.w3.org/2000/svg'>
@@ -4015,6 +4045,7 @@ export function AsanaIcon(props: SVGProps<SVGSVGElement>) {
}
export function PipedriveIcon(props: SVGProps<SVGSVGElement>) {
const pathId = useId()
return (
<svg
{...props}
@@ -4028,7 +4059,7 @@ export function PipedriveIcon(props: SVGProps<SVGSVGElement>) {
<defs>
<path
d='M59.6807,81.1772 C59.6807,101.5343 70.0078,123.4949 92.7336,123.4949 C109.5872,123.4949 126.6277,110.3374 126.6277,80.8785 C126.6277,55.0508 113.232,37.7119 93.2944,37.7119 C77.0483,37.7119 59.6807,49.1244 59.6807,81.1772 Z M101.3006,0 C142.0482,0 169.4469,32.2728 169.4469,80.3126 C169.4469,127.5978 140.584,160.60942 99.3224,160.60942 C79.6495,160.60942 67.0483,152.1836 60.4595,146.0843 C60.5063,147.5305 60.5374,149.1497 60.5374,150.8788 L60.5374,215 L18.32565,215 L18.32565,44.157 C18.32565,41.6732 17.53126,40.8873 15.07021,40.8873 L0.5531,40.8873 L0.5531,3.4741 L35.9736,3.4741 C52.282,3.4741 56.4564,11.7741 57.2508,18.1721 C63.8708,10.7524 77.5935,0 101.3006,0 Z'
id='path-1'
id={pathId}
/>
</defs>
<g
@@ -4039,10 +4070,7 @@ export function PipedriveIcon(props: SVGProps<SVGSVGElement>) {
fillRule='evenodd'
>
<g transform='translate(67.000000, 44.000000)'>
<mask id='mask-2' fill='white'>
<use href='#path-1' />
</mask>
<use id='Clip-5' fill='#FFFFFF' xlinkHref='#path-1' />
<use fill='#FFFFFF' xlinkHref={`#${pathId}`} />
</g>
</g>
</svg>
@@ -4068,6 +4096,40 @@ export function SalesforceIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function SapS4HanaIcon(props: SVGProps<SVGSVGElement>) {
const id = useId()
return (
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 412.38 204'>
<defs>
<linearGradient
id={id}
x1='206.19'
y1='0'
x2='206.19'
y2='204'
gradientUnits='userSpaceOnUse'
>
<stop offset='0' stopColor='#00b1eb' />
<stop offset='.212' stopColor='#009ad9' />
<stop offset='.519' stopColor='#007fc4' />
<stop offset='.792' stopColor='#006eb8' />
<stop offset='1' stopColor='#0069b4' />
</linearGradient>
</defs>
<polyline
fill={`url(#${id})`}
fillRule='evenodd'
points='0 204 208.413 204 412.38 0 0 0 0 204'
/>
<path
fill='#fff'
fillRule='evenodd'
d='m244.727,38.359l-40.593-.025v96.518l-35.46-96.518h-35.16l-30.277,80.716c-3.224-20.352-24.277-27.38-40.84-32.649-10.937-3.512-22.541-8.678-22.434-14.387.091-4.687,6.225-9.04,18.377-8.385,8.17.433,15.373,1.092,29.71,8.006l14.102-24.557c-13.088-6.658-31.169-10.867-45.985-10.883h-.086c-17.277,0-31.677,5.598-40.602,14.824-6.221,6.443-9.572,14.626-9.712,23.679-.227,12.454,4.341,21.292,13.938,28.338,8.104,5.944,18.468,9.794,27.603,12.626,11.27,3.492,20.467,6.526,20.36,13.002-.083,2.355-.977,4.552-2.671,6.337-2.807,2.897-7.124,3.986-13.084,4.098-11.497.243-20.026-1.559-33.61-9.585l-12.536,24.903c13.546,7.705,29.586,12.223,45.952,12.223l2.106-.024c14.247-.256,25.745-4.316,34.929-11.712.527-.416,1.001-.845,1.488-1.277l-4.073,10.874h36.875l6.189-18.822c6.477,2.214,13.847,3.437,21.676,3.437,7.618,0,14.795-1.17,21.156-3.252l5.965,18.637h60.137v-38.969h13.113c31.706,0,50.456-16.147,50.456-43.202,0-30.139-18.219-43.969-57.011-43.969Zm-93.816,82.587c-4.737,0-9.177-.828-13.006-2.275l12.866-40.593h.244l12.643,40.708c-3.801,1.349-8.138,2.16-12.746,2.16Zm96.199-23.324h-8.941v-32.711h8.941c11.927,0,21.437,3.961,21.437,16.139,0,12.602-9.51,16.572-21.437,16.572'
/>
</svg>
)
}
export function ServiceNowIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 71.1 63.6'>
@@ -4664,15 +4726,16 @@ export function DynamoDBIcon(props: SVGProps<SVGSVGElement>) {
}
export function IAMIcon(props: SVGProps<SVGSVGElement>) {
const id = useId()
return (
<svg {...props} viewBox='0 0 80 80' xmlns='http://www.w3.org/2000/svg'>
<defs>
<linearGradient x1='0%' y1='100%' x2='100%' y2='0%' id='iamGradient'>
<linearGradient x1='0%' y1='100%' x2='100%' y2='0%' id={id}>
<stop stopColor='#BD0816' offset='0%' />
<stop stopColor='#FF5252' offset='100%' />
</linearGradient>
</defs>
<rect fill='url(#iamGradient)' width='80' height='80' />
<rect fill={`url(#${id})`} width='80' height='80' />
<path
d='M14,59 L66,59 L66,21 L14,21 L14,59 Z M68,20 L68,60 C68,60.552 67.553,61 67,61 L13,61 C12.447,61 12,60.552 12,60 L12,20 C12,19.448 12.447,19 13,19 L67,19 C67.553,19 68,19.448 68,20 L68,20 Z M44,48 L59,48 L59,46 L44,46 L44,48 Z M57,42 L62,42 L62,40 L57,40 L57,42 Z M44,42 L52,42 L52,40 L44,40 L44,42 Z M29,46 C29,45.449 28.552,45 28,45 C27.448,45 27,45.449 27,46 C27,46.551 27.448,47 28,47 C28.552,47 29,46.551 29,46 L29,46 Z M31,46 C31,47.302 30.161,48.401 29,48.816 L29,51 L27,51 L27,48.815 C25.839,48.401 25,47.302 25,46 C25,44.346 26.346,43 28,43 C29.654,43 31,44.346 31,46 L31,46 Z M19,53.993 L36.994,54 L36.996,50 L33,50 L33,48 L36.996,48 L36.998,45 L33,45 L33,43 L36.999,43 L37,40.007 L19.006,40 L19,53.993 Z M22,38.001 L34,38.006 L34,31 C34.001,28.697 31.197,26.677 28,26.675 L27.996,26.675 C24.804,26.675 22.004,28.696 22.002,31 L22,38.001 Z M17,54.992 L17.006,39 C17.006,38.734 17.111,38.48 17.299,38.292 C17.486,38.105 17.741,38 18.006,38 L20,38.001 L20.002,31 C20.004,27.512 23.59,24.675 27.996,24.675 L28,24.675 C32.412,24.677 36.001,27.515 36,31 L36,38.007 L38,38.008 C38.553,38.008 39,38.456 39,39.008 L38.994,55 C38.994,55.266 38.889,55.52 38.701,55.708 C38.514,55.895 38.259,56 37.994,56 L18,55.992 C17.447,55.992 17,55.544 17,54.992 L17,54.992 Z M60,36 L62,36 L62,34 L60,34 L60,36 Z M44,36 L55,36 L55,34 L44,34 L44,36 Z'
fill='#FFFFFF'
@@ -4682,10 +4745,18 @@ export function IAMIcon(props: SVGProps<SVGSVGElement>) {
}
export function IdentityCenterIcon(props: SVGProps<SVGSVGElement>) {
const id = useId()
return (
<svg {...props} viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'>
<svg {...props} viewBox='0 0 80 80' xmlns='http://www.w3.org/2000/svg'>
<defs>
<linearGradient x1='0%' y1='100%' x2='100%' y2='0%' id={id}>
<stop stopColor='#BD0816' offset='0%' />
<stop stopColor='#FF5252' offset='100%' />
</linearGradient>
</defs>
<rect fill={`url(#${id})`} width='80' height='80' />
<path
d='M13.694,14.8194562 C14.376,14.1374562 14.376,13.0294562 13.694,12.3474562 C13.353,12.0074562 12.906,11.8374562 12.459,11.8374562 C12.01,11.8374562 11.563,12.0074562 11.222,12.3474562 C10.542,13.0284562 10.542,14.1384562 11.222,14.8194562 C11.905,15.5014562 13.013,15.4994562 13.694,14.8194562 M14.718,15.1374562 L18.703,19.1204562 L17.996,19.8274562 L16.868,18.6994562 L15.793,19.7754562 L15.086,19.0684562 L16.161,17.9924562 L14.011,15.8444562 C13.545,16.1654562 13.003,16.3294562 12.458,16.3294562 C11.755,16.3294562 11.051,16.0624562 10.515,15.5264562 C9.445,14.4554562 9.445,12.7124562 10.515,11.6404562 C11.586,10.5714562 13.329,10.5694562 14.401,11.6404562 C15.351,12.5904562 15.455,14.0674562 14.718,15.1374562 M20,12.1014562 C20,14.1684562 18.505,15.0934562 17.023,15.0934562 L17.023,14.0934562 C17.487,14.0934562 19,13.9494562 19,12.1014562 C19,11.0044562 18.353,10.3894562 16.905,10.1084562 C16.68,10.0654562 16.514,9.87545615 16.501,9.64845615 C16.446,8.74445615 15.987,8.11245615 15.384,8.11245615 C15.084,8.11245615 14.854,8.24245615 14.616,8.54645615 C14.506,8.68845615 14.324,8.75945615 14.147,8.73245615 C13.968,8.70545615 13.818,8.58445615 13.755,8.41445615 C13.577,7.94345615 13.211,7.43345615 12.723,6.97745615 C12.231,6.50945615 10.883,5.50745615 8.972,6.27345615 C7.885,6.70545615 7.034,7.94945615 7.034,9.10745615 C7.034,9.23545615 7.043,9.36245615 7.058,9.48845615 C7.061,9.50945615 7.062,9.53045615 7.062,9.55145615 C7.062,9.79945615 6.882,10.0064562 6.645,10.0464562 C5.886,10.2394562 5,10.7454562 5,12.0554562 L5.005,12.2104562 C5.069,13.3254562 6.252,13.9954562 7.358,13.9984562 L8,13.9984562 L8,14.9984562 L7.357,14.9984562 C5.536,14.9944562 4.095,13.8194562 4.006,12.2644562 C4.003,12.1944562 4,12.1244562 4,12.0554562 C4,10.6944562 4.752,9.64845615 6.035,9.18845615 C6.034,9.16145615 6.034,9.13445615 6.034,9.10745615 C6.034,7.54345615 7.138,5.92545615 8.602,5.34345615 C10.298,4.66545615 12.095,5.00345615 13.409,6.24945615 C13.706,6.52745615 14.076,6.92645615 14.372,7.41345615 C14.673,7.21245615 15.008,7.11245615 15.384,7.11245615 C16.257,7.11245615 17.231,7.77145615 17.458,9.20745615 C19.145,9.63245615 20,10.6054562 20,12.1014562'
d='M46.694,46.8194562 C47.376,46.1374562 47.376,45.0294562 46.694,44.3474562 C46.353,44.0074562 45.906,43.8374562 45.459,43.8374562 C45.01,43.8374562 44.563,44.0074562 44.222,44.3474562 C43.542,45.0284562 43.542,46.1384562 44.222,46.8194562 C44.905,47.5014562 46.013,47.4994562 46.694,46.8194562 M47.718,47.1374562 L51.703,51.1204562 L50.996,51.8274562 L49.868,50.6994562 L48.793,51.7754562 L48.086,51.0684562 L49.161,49.9924562 L47.011,47.8444562 C46.545,48.1654562 46.003,48.3294562 45.458,48.3294562 C44.755,48.3294562 44.051,48.0624562 43.515,47.5264562 C42.445,46.4554562 42.445,44.7124562 43.515,43.6404562 C44.586,42.5714562 46.329,42.5694562 47.401,43.6404562 C48.351,44.5904562 48.455,46.0674562 47.718,47.1374562 M53,44.1014562 C53,46.1684562 51.505,47.0934562 50.023,47.0934562 L50.023,46.0934562 C50.487,46.0934562 52,45.9494562 52,44.1014562 C52,43.0044562 51.353,42.3894562 49.905,42.1084562 C49.68,42.0654562 49.514,41.8754562 49.501,41.6484562 C49.446,40.7444562 48.987,40.1124562 48.384,40.1124562 C48.084,40.1124562 47.854,40.2424562 47.616,40.5464562 C47.506,40.6884562 47.324,40.7594562 47.147,40.7324562 C46.968,40.7054562 46.818,40.5844562 46.755,40.4144562 C46.577,39.9434562 46.211,39.4334562 45.723,38.9774562 C45.231,38.5094562 43.883,37.5074562 41.972,38.2734562 C40.885,38.7054562 40.034,39.9494562 40.034,41.1074562 C40.034,41.2354562 40.043,41.3624562 40.058,41.4884562 C40.061,41.5094562 40.062,41.5304562 40.062,41.5514562 C40.062,41.7994562 39.882,42.0064562 39.645,42.0464562 C38.886,42.2394562 38,42.7454562 38,44.0554562 L38.005,44.2104562 C38.069,45.3254562 39.252,45.9954562 40.358,45.9984562 L41,45.9984562 L41,46.9984562 L40.357,46.9984562 C38.536,46.9944562 37.095,45.8194562 37.006,44.2644562 C37.003,44.1944562 37,44.1244562 37,44.0554562 C37,42.6944562 37.752,41.6484562 39.035,41.1884562 C39.034,41.1614562 39.034,41.1344562 39.034,41.1074562 C39.034,39.5434562 40.138,37.9254562 41.602,37.3434562 C43.298,36.6654562 45.095,37.0034562 46.409,38.2494562 C46.706,38.5274562 47.076,38.9264562 47.372,39.4134562 C47.673,39.2124562 48.008,39.1124562 48.384,39.1124562 C49.257,39.1124562 50.231,39.7714562 50.458,41.2074562 C52.145,41.6324562 53,42.6054562 53,44.1014562 M27,53 L27,27 L53,27 L53,34 L51,34 L51,29 L29,29 L29,51 L51,51 L51,46 L53,46 L53,53 Z'
fill='#FFFFFF'
/>
</svg>
@@ -4693,15 +4764,16 @@ export function IdentityCenterIcon(props: SVGProps<SVGSVGElement>) {
}
export function STSIcon(props: SVGProps<SVGSVGElement>) {
const id = useId()
return (
<svg {...props} viewBox='0 0 80 80' xmlns='http://www.w3.org/2000/svg'>
<defs>
<linearGradient x1='0%' y1='100%' x2='100%' y2='0%' id='stsGradient'>
<linearGradient x1='0%' y1='100%' x2='100%' y2='0%' id={id}>
<stop stopColor='#BD0816' offset='0%' />
<stop stopColor='#FF5252' offset='100%' />
</linearGradient>
</defs>
<rect fill='url(#stsGradient)' width='80' height='80' />
<rect fill={`url(#${id})`} width='80' height='80' />
<path
d='M14,59 L66,59 L66,21 L14,21 L14,59 Z M68,20 L68,60 C68,60.552 67.553,61 67,61 L13,61 C12.447,61 12,60.552 12,60 L12,20 C12,19.448 12.447,19 13,19 L67,19 C67.553,19 68,19.448 68,20 L68,20 Z M44,48 L59,48 L59,46 L44,46 L44,48 Z M57,42 L62,42 L62,40 L57,40 L57,42 Z M44,42 L52,42 L52,40 L44,40 L44,42 Z M29,46 C29,45.449 28.552,45 28,45 C27.448,45 27,45.449 27,46 C27,46.551 27.448,47 28,47 C28.552,47 29,46.551 29,46 L29,46 Z M31,46 C31,47.302 30.161,48.401 29,48.816 L29,51 L27,51 L27,48.815 C25.839,48.401 25,47.302 25,46 C25,44.346 26.346,43 28,43 C29.654,43 31,44.346 31,46 L31,46 Z M19,53.993 L36.994,54 L36.996,50 L33,50 L33,48 L36.996,48 L36.998,45 L33,45 L33,43 L36.999,43 L37,40.007 L19.006,40 L19,53.993 Z M22,38.001 L34,38.006 L34,31 C34.001,28.697 31.197,26.677 28,26.675 L27.996,26.675 C24.804,26.675 22.004,28.696 22.002,31 L22,38.001 Z M17,54.992 L17.006,39 C17.006,38.734 17.111,38.48 17.299,38.292 C17.486,38.105 17.741,38 18.006,38 L20,38.001 L20.002,31 C20.004,27.512 23.59,24.675 27.996,24.675 L28,24.675 C32.412,24.677 36.001,27.515 36,31 L36,38.007 L38,38.008 C38.553,38.008 39,38.456 39,39.008 L38.994,55 C38.994,55.266 38.889,55.52 38.701,55.708 C38.514,55.895 38.259,56 37.994,56 L18,55.992 C17.447,55.992 17,55.544 17,54.992 L17,54.992 Z M60,36 L62,36 L62,34 L60,34 L60,36 Z M44,36 L55,36 L55,34 L44,34 L44,36 Z'
fill='#FFFFFF'
@@ -4711,15 +4783,16 @@ export function STSIcon(props: SVGProps<SVGSVGElement>) {
}
export function SESIcon(props: SVGProps<SVGSVGElement>) {
const id = useId()
return (
<svg {...props} viewBox='0 0 80 80' xmlns='http://www.w3.org/2000/svg'>
<defs>
<linearGradient x1='0%' y1='100%' x2='100%' y2='0%' id='sesGradient'>
<linearGradient x1='0%' y1='100%' x2='100%' y2='0%' id={id}>
<stop stopColor='#BD0816' offset='0%' />
<stop stopColor='#FF5252' offset='100%' />
</linearGradient>
</defs>
<rect fill='url(#sesGradient)' width='80' height='80' />
<rect fill={`url(#${id})`} width='80' height='80' />
<path
d='M57,60.999875 C57,59.373846 55.626,57.9998214 54,57.9998214 C52.374,57.9998214 51,59.373846 51,60.999875 C51,62.625904 52.374,63.9999286 54,63.9999286 C55.626,63.9999286 57,62.625904 57,60.999875 L57,60.999875 Z M40,59.9998571 C38.374,59.9998571 37,61.3738817 37,62.9999107 C37,64.6259397 38.374,65.9999643 40,65.9999643 C41.626,65.9999643 43,64.6259397 43,62.9999107 C43,61.3738817 41.626,59.9998571 40,59.9998571 L40,59.9998571 Z M26,57.9998214 C24.374,57.9998214 23,59.373846 23,60.999875 C23,62.625904 24.374,63.9999286 26,63.9999286 C27.626,63.9999286 29,62.625904 29,60.999875 C29,59.373846 27.626,57.9998214 26,57.9998214 L26,57.9998214 Z M28.605,42.9995536 L51.395,42.9995536 L43.739,36.1104305 L40.649,38.7584778 C40.463,38.9194807 40.23,38.9994821 39.999,38.9994821 C39.768,38.9994821 39.535,38.9194807 39.349,38.7584778 L36.26,36.1104305 L28.605,42.9995536 Z M27,28.1732888 L27,41.7545313 L34.729,34.7984071 L27,28.1732888 Z M51.297,26.9992678 L28.703,26.9992678 L39.999,36.6824408 L51.297,26.9992678 Z M53,41.7545313 L53,28.1732888 L45.271,34.7974071 L53,41.7545313 Z M59,60.999875 C59,63.7099234 56.71,65.9999643 54,65.9999643 C51.29,65.9999643 49,63.7099234 49,60.999875 C49,58.6308327 50.75,56.5837961 53,56.1057876 L53,52.9997321 L41,52.9997321 L41,58.1058233 C43.25,58.5838319 45,60.6308684 45,62.9999107 C45,65.7099591 42.71,68 40,68 C37.29,68 35,65.7099591 35,62.9999107 C35,60.6308684 36.75,58.5838319 39,58.1058233 L39,52.9997321 L27,52.9997321 L27,56.1057876 C29.25,56.5837961 31,58.6308327 31,60.999875 C31,63.7099234 28.71,65.9999643 26,65.9999643 C23.29,65.9999643 21,63.7099234 21,60.999875 C21,58.6308327 22.75,56.5837961 25,56.1057876 L25,51.9997143 C25,51.4477044 25.447,50.9996964 26,50.9996964 L39,50.9996964 L39,44.9995893 L26,44.9995893 C25.447,44.9995893 25,44.5515813 25,43.9995714 L25,25.99925 C25,25.4472401 25.447,24.9992321 26,24.9992321 L54,24.9992321 C54.553,24.9992321 55,25.4472401 55,25.99925 L55,43.9995714 C55,44.5515813 54.553,44.9995893 54,44.9995893 L41,44.9995893 L41,50.9996964 L54,50.9996964 C54.553,50.9996964 55,51.4477044 55,51.9997143 L55,56.1057876 C57.25,56.5837961 59,58.6308327 59,60.999875 L59,60.999875 Z M68,39.9995 C68,45.9066055 66.177,51.5597064 62.727,56.3447919 L61.104,55.174771 C64.307,50.7316916 66,45.4845979 66,39.9995 C66,25.664244 54.337,14.0000357 40.001,14.0000357 C25.664,14.0000357 14,25.664244 14,39.9995 C14,45.4845979 15.693,50.7316916 18.896,55.174771 L17.273,56.3447919 C13.823,51.5597064 12,45.9066055 12,39.9995 C12,24.5612243 24.561,12 39.999,12 C55.438,12 68,24.5612243 68,39.9995 L68,39.9995 Z'
fill='#FFFFFF'
@@ -4729,15 +4802,16 @@ export function SESIcon(props: SVGProps<SVGSVGElement>) {
}
export function SecretsManagerIcon(props: SVGProps<SVGSVGElement>) {
const id = useId()
return (
<svg {...props} viewBox='0 0 80 80' xmlns='http://www.w3.org/2000/svg'>
<defs>
<linearGradient x1='0%' y1='100%' x2='100%' y2='0%' id='secretsManagerGradient'>
<linearGradient x1='0%' y1='100%' x2='100%' y2='0%' id={id}>
<stop stopColor='#BD0816' offset='0%' />
<stop stopColor='#FF5252' offset='100%' />
</linearGradient>
</defs>
<rect fill='url(#secretsManagerGradient)' width='80' height='80' />
<rect fill={`url(#${id})`} width='80' height='80' />
<path
d='M38.76,43.36 C38.76,44.044 39.317,44.6 40,44.6 C40.684,44.6 41.24,44.044 41.24,43.36 C41.24,42.676 40.684,42.12 40,42.12 C39.317,42.12 38.76,42.676 38.76,43.36 L38.76,43.36 Z M36.76,43.36 C36.76,41.573 38.213,40.12 40,40.12 C41.787,40.12 43.24,41.573 43.24,43.36 C43.24,44.796 42.296,46.002 41,46.426 L41,49 L39,49 L39,46.426 C37.704,46.002 36.76,44.796 36.76,43.36 L36.76,43.36 Z M49,38 L31,38 L31,51 L49,51 L49,48 L46,48 L46,46 L49,46 L49,43 L46,43 L46,41 L49,41 L49,38 Z M34,36 L45.999,36 L46,31 C46.001,28.384 43.143,26.002 40.004,26 L40.001,26 C38.472,26 36.928,26.574 35.763,27.575 C34.643,28.537 34,29.786 34,31.001 L34,36 Z M48,31.001 L47.999,36 L50,36 C50.553,36 51,36.448 51,37 L51,52 C51,52.552 50.553,53 50,53 L30,53 C29.447,53 29,52.552 29,52 L29,37 C29,36.448 29.447,36 30,36 L32,36 L32,31 C32.001,29.202 32.897,27.401 34.459,26.058 C35.982,24.75 38.001,24 40.001,24 L40.004,24 C44.265,24.002 48.001,27.273 48,31.001 L48,31.001 Z M19.207,55.049 L20.828,53.877 C18.093,50.097 16.581,45.662 16.396,41 L19,41 L19,39 L16.399,39 C16.598,34.366 18.108,29.957 20.828,26.198 L19.207,25.025 C16.239,29.128 14.599,33.942 14.399,39 L12,39 L12,41 L14.396,41 C14.582,46.086 16.224,50.926 19.207,55.049 L19.207,55.049 Z M53.838,59.208 C50.069,61.936 45.648,63.446 41,63.639 L41,61 L39,61 L39,63.639 C34.352,63.447 29.93,61.937 26.159,59.208 L24.988,60.828 C29.1,63.805 33.928,65.445 39,65.639 L39,68 L41,68 L41,65.639 C46.072,65.445 50.898,63.805 55.01,60.828 L53.838,59.208 Z M26.159,20.866 C29.93,18.138 34.352,16.628 39,16.436 L39,19 L41,19 L41,16.436 C45.648,16.628 50.069,18.138 53.838,20.866 L55.01,19.246 C50.898,16.27 46.072,14.63 41,14.436 L41,12 L39,12 L39,14.436 C33.928,14.629 29.1,16.269 24.988,19.246 L26.159,20.866 Z M65.599,39 C65.399,33.942 63.759,29.128 60.79,25.025 L59.169,26.198 C61.89,29.957 63.4,34.366 63.599,39 L61,39 L61,41 L63.602,41 C63.416,45.662 61.905,50.097 59.169,53.877 L60.79,55.049 C63.774,50.926 65.415,46.086 65.602,41 L68,41 L68,39 L65.599,39 Z M56.386,25.064 L64.226,17.224 L62.812,15.81 L54.972,23.65 L56.386,25.064 Z M23.612,55.01 L15.772,62.85 L17.186,64.264 L25.026,56.424 L23.612,55.01 Z M28.666,27.253 L13.825,12.413 L12.411,13.827 L27.252,28.667 L28.666,27.253 Z M54.193,52.78 L67.586,66.173 L66.172,67.587 L52.779,54.194 L54.193,52.78 Z'
fill='#FFFFFF'

View File

@@ -6,6 +6,7 @@ import type { ComponentType, SVGProps } from 'react'
import {
A2AIcon,
AgentMailIcon,
AgentPhoneIcon,
AgiloftIcon,
AhrefsIcon,
AirtableIcon,
@@ -153,6 +154,7 @@ import {
RootlyIcon,
S3Icon,
SalesforceIcon,
SapS4HanaIcon,
SESIcon,
SearchIcon,
SecretsManagerIcon,
@@ -204,6 +206,7 @@ type IconComponent = ComponentType<SVGProps<SVGSVGElement>>
export const blockTypeToIconMap: Record<string, IconComponent> = {
a2a: A2AIcon,
agentmail: AgentMailIcon,
agentphone: AgentPhoneIcon,
agiloft: AgiloftIcon,
ahrefs: AhrefsIcon,
airtable: AirtableIcon,
@@ -367,6 +370,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
rootly: RootlyIcon,
s3: S3Icon,
salesforce: SalesforceIcon,
sap_s4hana: SapS4HanaIcon,
search: SearchIcon,
secrets_manager: SecretsManagerIcon,
sendgrid: SendgridIcon,

View File

@@ -25,6 +25,8 @@ Secrets are organized into two sections:
- **Workspace** — shared with all members of your workspace
- **Personal** — private to you
External workspace members count as workspace members for workspace-scoped secrets. They can use workspace secrets according to their workspace permission level, even though they are not members of your organization.
### Adding a Secret
Type a key name (e.g. `OPENAI_API_KEY`) into the **Key** column and its value into the **Value** column in the last empty row. A new empty row appears automatically as you type. Existing values are masked by default.
@@ -89,7 +91,7 @@ Click **Save** to apply changes, or **Back** to return to the list.
| | Workspace | Personal |
|---|---|---|
| **Visibility** | All workspace members | Only you |
| **Visibility** | All workspace members, including external workspace members | Only you |
| **Use in workflows** | Any member can use | Only you can use |
| **Best for** | Production workflows, shared services | Testing, personal API keys |
| **Who can edit** | Workspace admins | Only you |

View File

@@ -130,6 +130,8 @@ Controls visibility of platform features and modules.
Open the group's **Details** view and add members by searching for users by name or email. Only users who already have workspace-level access can be added. A user can only belong to one group per workspace — adding a user to a new group within the same workspace removes them from their current group for that workspace.
External workspace members are treated like other workspace members for access-control purposes. They can be assigned to permission groups in any workspace they have access to, but they do not become organization members or appear in the organization roster.
---
## Enforcement
@@ -159,6 +161,7 @@ When a user opens Mothership, their permission group is read before any block or
- Moving a user to a new group within a workspace automatically removes them from their previous group in that workspace.
- Users not assigned to any group in a workspace have no restrictions applied in that workspace (all blocks, providers, and features are available to them there).
- If **Auto-add new members** is enabled on a group, new members of that workspace are automatically placed in the group. Only one group per workspace can have this setting active.
- External workspace members follow the same per-workspace permission group rules as internal members.
---

View File

@@ -44,7 +44,7 @@ Authorization: Bearer <api-key>
| `resourceType` | string | Filter by resource type (e.g. `workflow`) |
| `resourceId` | string | Filter by a specific resource ID |
| `workspaceId` | string | Filter by workspace |
| `actorId` | string | Filter by user ID (must be an org member) |
| `actorId` | string | Filter by user ID. For organization-wide filters, the actor must be a current or former org member; workspace-scoped logs can also include external workspace members. |
| `startDate` | string | ISO 8601 date — return logs on or after this date |
| `endDate` | string | ISO 8601 date — return logs on or before this date |
| `includeDeparted` | boolean | Include logs from members who have since left the organization (default `false`) |
@@ -98,6 +98,8 @@ Audit log events follow a `resource.action` naming pattern. The table below list
| **Credentials** | `credential.created`, `credential.deleted`, `oauth.disconnected` |
| **Organization** | `organization.updated`, `org_member.added`, `org_member.role_changed` |
Workspace invitation events include whether the invite is for an internal organization member or an external workspace member in their metadata. External workspace members can appear as actors on workspace-scoped events, but they are not organization members and do not appear in the organization roster.
---
<FAQ items={[
@@ -123,7 +125,7 @@ Audit log events follow a `resource.action` naming pattern. The table below list
},
{
question: "Can I filter logs by a specific user?",
answer: "Yes. Pass the actorId query parameter to filter logs by a specific user. The actor must be a current or former member of your organization."
answer: "Yes. Pass the actorId query parameter to filter logs by a specific user. Organization-wide actor filters require the actor to be a current or former member of your organization. Workspace-scoped logs may also include external workspace members who acted inside a workspace without joining the organization."
}
]} />

View File

@@ -6,7 +6,7 @@ description: Control how long execution logs, deleted resources, and copilot dat
import { FAQ } from '@/components/ui/faq'
import { Image } from '@/components/ui/image'
Data Retention lets workspace admins on Enterprise plans configure how long three categories of data are kept before they are permanently deleted. Each workspace in your organization can have its own independent configuration.
Data Retention lets organization owners and admins on Enterprise plans configure how long three categories of data are kept before they are permanently deleted. The configuration applies to every workspace in the organization.
---
@@ -58,9 +58,9 @@ Each setting is independent. You can configure a short log retention period alon
---
## Per-workspace configuration
## Organization-wide configuration
Retention is configured at the **workspace level**, not organization-wide. Each workspace in your organization can have a different configuration. Changes to one workspace's settings do not affect other workspaces.
Retention is configured at the **organization level**. A single configuration applies to every workspace in the organization — there are no per-workspace overrides.
---
@@ -73,7 +73,7 @@ By default, all three settings are unconfigured — no data is automatically del
<FAQ items={[
{
question: "Who can configure data retention settings?",
answer: "Only workspace admins can configure data retention settings. On Sim Cloud, the workspace must be on an Enterprise plan."
answer: "Only organization owners and admins can configure data retention settings. On Sim Cloud, the organization must be on an Enterprise plan."
},
{
question: "Is deletion immediate once the retention period expires?",
@@ -85,7 +85,7 @@ By default, all three settings are unconfigured — no data is automatically del
},
{
question: "Does the retention period apply to all workspaces in my organization?",
answer: "No. Retention is configured per workspace. Each workspace in your organization can have a different configuration."
answer: "Yes. Retention is configured once per organization and applies to every workspace in the organization."
},
{
question: "What happens if I shorten the retention period?",

View File

@@ -13,6 +13,8 @@ Sim Enterprise provides advanced features for organizations with enhanced securi
Define permission groups on a workspace to control what features and integrations its members can use. Permission groups are scoped to a single workspace — a user can belong to different groups (or no group) in different workspaces.
External workspace members can be assigned to permission groups just like internal organization members, but they remain outside the organization roster and do not consume seats.
### Features
- **Allowed Model Providers** - Restrict which AI providers users can access (OpenAI, Anthropic, Google, etc.)
@@ -81,4 +83,4 @@ Self-hosted deployments enable enterprise features via environment variables ins
| `INBOX_ENABLED`, `NEXT_PUBLIC_INBOX_ENABLED` | Sim Mailer inbox |
| `DISABLE_INVITATIONS`, `NEXT_PUBLIC_DISABLE_INVITATIONS` | Disable invitations; manage membership via Admin API |
Once enabled, each feature is configured through the same Settings UI as Sim Cloud. When invitations are disabled, use the Admin API (`x-admin-key` header) to manage organization and workspace membership.
Once enabled, each feature is configured through the same Settings UI as Sim Cloud. When invitations are disabled, use the Admin API (`x-admin-key` header) to manage organization membership and workspace access. Internal members join the organization; external workspace members only receive access to a specific workspace.

View File

@@ -221,6 +221,8 @@ Once SSO is configured, users with your domain (`company.com`) can sign in throu
Users who sign in via SSO for the first time are automatically provisioned and added to your organization — no manual invite required.
SSO provisioning creates internal organization members. External workspace members are different: they are invited to a specific workspace without joining your organization or consuming one of your seats.
<Callout type="info">
Password-based login remains available. Forcing all organization members to use SSO exclusively is not yet supported.
</Callout>
@@ -242,7 +244,7 @@ Users who sign in via SSO for the first time are automatically provisioned and a
},
{
question: "What happens when a user signs in with SSO for the first time?",
answer: "Sim creates an account for them automatically and adds them to your organization. No manual invite is needed. They are assigned the member role by default."
answer: "Sim creates an account for them automatically and adds them to your organization. No manual invite is needed. They are assigned the member role by default. External workspace members are not provisioned through SSO into your organization; they are invited directly to a workspace and remain outside your org roster."
},
{
question: "Can I still use email/password login after enabling SSO?",

View File

@@ -272,6 +272,8 @@ Sim has two paid plan tiers - **Pro** and **Max**. Either can be used individual
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
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.
@@ -317,7 +319,7 @@ By default, your usage is capped at the credits included in your plan. To allow
| **Max** | Up to 10 | — |
| **Team / Enterprise** | Unlimited | Unlimited |
Team and Enterprise plans unlock shared workspaces that belong to your organization. Members invited to a shared workspace automatically join the organization and count toward your seat total. When a Team or Enterprise subscription is cancelled or downgraded, existing shared workspaces remain accessible to current members but new invites are disabled until the organization is upgraded again.
Team and Enterprise plans unlock shared workspaces that belong to your organization. Internal members invited to a shared workspace join the organization and count toward your seat total. Existing Sim users who already belong to another organization can be added as external workspace members; they get workspace access without joining your organization or using one of your seats. When a Team or Enterprise subscription is cancelled or downgraded, existing shared workspaces remain accessible to current members but new invites are disabled until the organization is upgraded again.
### Rate Limits
@@ -368,7 +370,8 @@ Sim uses a **base subscription + overage** billing model:
- Example: 7,000 credits used = $25 (subscription) + $5 (overage for 1,000 extra credits at $0.005/credit)
**Team Plans:**
- Usage is pooled across all team members in the organization
- Usage is pooled across internal team members in the organization
- External workspace members keep their own organization or personal billing context for runs where they are the billing actor
- Overage is calculated from total team usage against the pooled limit
- Organization owner receives one bill

View File

@@ -42,6 +42,8 @@ Only authorized senders can create tasks. Emails from anyone else are automatica
- **Workspace members** are allowed by default — no setup needed
- **External senders** can be added manually with an optional label for easy identification
External senders are email addresses that can create inbox tasks. They are not the same as external workspace members, who have workspace access in Sim without joining your organization.
Manage your allowed senders list in **Settings** → **Inbox** → **Allowed Senders**.
## Tracking Tasks

View File

@@ -12,7 +12,7 @@ When you invite team members to your organization or workspace, you'll need to c
Sim has two kinds of workspaces:
- **Personal workspaces** live under your individual account. The number you can create depends on your plan.
- **Shared (organization) workspaces** live under an organization and are available on Team and Enterprise plans. Any organization Owner or Admin can create them. Members invited to a shared workspace automatically join the organization and count toward your seat total.
- **Shared (organization) workspaces** live under an organization and are available on Team and Enterprise plans. Any organization Owner or Admin can create them. Internal members invited to a shared workspace join the organization and count toward your seat total. Existing Sim users who already belong to another organization can be added as external workspace members instead, giving them access to the workspace without adding them to your organization roster or using one of your seats.
### Workspace Limits by Plan
@@ -43,6 +43,15 @@ When inviting someone to a workspace, you can assign one of three permission lev
| **Write** | Create and edit workflows, run workflows, manage environment variables |
| **Admin** | Everything Write can do, plus invite/remove users and manage workspace settings |
## Internal Members vs External Workspace Members
Workspace permissions are separate from organization membership:
- **Internal organization members** belong to your organization, appear in the organization roster, and count toward your seat total. Invite new teammates this way when they should be part of your company or team in Sim.
- **External workspace members** have access only to the workspace they are invited to. They keep their own organization membership, do not appear in your organization roster, and do not count toward your organization's seats. Use external access for clients, partners, contractors, or collaborators who already use Sim in another organization.
External workspace members still receive a workspace permission level — Read, Write, or Admin — and that permission controls what they can do inside the workspace.
## What Each Permission Level Can Do
Here's a detailed breakdown of what users can do with each permission level:
@@ -126,7 +135,7 @@ Every workspace has one **Owner** (the person who created it) plus any number of
2. **Workspace level**: Give them **Admin** permission so they can manage the team and see everything
### Adding a Stakeholder or Client
1. **Organization level**: Invite them as an **Organization Member**
1. **Organization level**: If they should not join your organization, add them as an **External workspace member**
2. **Workspace level**: Give them **Read** permission so they can see progress but not make changes
---
@@ -199,12 +208,12 @@ An organization has three roles: **Owner**, **Admin**, and **Member**.
import { FAQ } from '@/components/ui/faq'
<FAQ items={[
{ question: "What is the difference between organization roles and workspace permissions?", answer: "Organization roles (Owner, Admin, or Member) control who can manage the organization itself, including inviting people, creating shared workspaces, and handling billing. Workspace permissions (Read, Write, Admin) control what a user can do within a specific workspace, such as viewing, editing, or managing workflows. A user needs both an organization role and a workspace permission to work within a shared workspace." },
{ question: "What is the difference between organization roles and workspace permissions?", answer: "Organization roles (Owner, Admin, or Member) control who can manage the organization itself, including inviting people, creating shared workspaces, and handling billing. Workspace permissions (Read, Write, Admin) control what a user can do within a specific workspace, such as viewing, editing, or managing workflows. Internal members need both an organization role and a workspace permission to work within a shared workspace. External workspace members do not have an organization role in your org; they only have workspace-level access." },
{ question: "How many workspaces can I create?", answer: "Free users get 1 personal workspace. Pro users get up to 3 personal workspaces. Max users get up to 10 personal workspaces. Team and Enterprise plans support unlimited shared workspaces under the organization — new invites are gated by your seat count." },
{ question: "What happens to my shared workspaces if I cancel or downgrade my Team plan?", answer: "Existing shared workspaces remain accessible to current members, but new invitations are disabled until you upgrade back to a Team or Enterprise plan. No workspaces or members are deleted — the organization is simply dormant until billing is re-enabled." },
{ question: "Can I restrict which integrations or model providers a team member can use?", answer: "Yes, on Enterprise-entitled workspaces. Any workspace admin can create permission groups with fine-grained controls, including restricting allowed integrations and allowed model providers to specific lists. You can also disable access to MCP tools, custom tools, skills, and various platform features like the knowledge base, API keys, or Copilot on a per-group basis. Permission groups are scoped per workspace — a user can belong to different groups in different workspaces." },
{ question: "What happens when a personal environment variable has the same name as a workspace variable?", answer: "The personal environment variable takes priority. When a workflow runs, if both a personal and workspace variable share the same name, the personal value is used. This allows individual users to override shared workspace configuration when needed." },
{ question: "Can an Admin remove the workspace owner?", answer: "No. The workspace owner cannot be removed from the workspace by anyone. Only the workspace owner can delete the workspace or transfer ownership to another user. Admins can do everything else, including inviting and removing other users and managing workspace settings." },
{ question: "What are permission groups and how do they work?", answer: "Permission groups are an Enterprise access control feature that lets workspace admins define granular restrictions beyond the standard Read/Write/Admin roles. Groups are scoped to a single workspace: each user can be in at most one group per workspace, and a user can be in different groups across different workspaces. A permission group can hide UI sections (like trace spans, knowledge base, API keys, or deployment options), disable features (MCP tools, custom tools, skills, invitations), and restrict which integrations and model providers its members can access. Members can be assigned manually, and new members can be auto-added on join. Execution-time enforcement is based on the workflow's workspace, not the user's current UI context." },
{ question: "How should I set up permissions for a new team member?", answer: "Start with the lowest permission level they need. Invite them to the organization as a Member, then add them to the relevant workspace with Read permission if they only need visibility, Write if they need to create and run workflows, or Admin if they need to manage the workspace and its users. You can always increase permissions later." },
{ question: "How should I set up permissions for a new team member?", answer: "Start with the lowest permission level they need. Invite teammates to the organization as Members, then add them to the relevant workspace with Read permission if they only need visibility, Write if they need to create and run workflows, or Admin if they need to manage the workspace and its users. For clients, partners, or users who already belong to another Sim organization, use external workspace access so they can collaborate without joining your organization or consuming a seat." },
]} />

View File

@@ -31,7 +31,7 @@ A quick lookup for everyday actions in the Sim workflow editor. For keyboard sho
</tr>
<tr>
<td>Invite team members</td>
<td>Sidebar → **Invite**</td>
<td>Sidebar → **Invite**. Internal invites join the organization; external workspace members get workspace access only.</td>
<td><ActionVideo src="quick-reference/invite.mp4" alt="Invite team members" /></td>
</tr>
<tr>

View File

@@ -0,0 +1,629 @@
---
title: AgentPhone
description: Provision numbers, send SMS and iMessage, and place voice calls with AgentPhone
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="agentphone"
color="linear-gradient(135deg, #1a1a1a 0%, #0a2a14 100%)"
/>
{/* MANUAL-CONTENT-START:intro */}
[AgentPhone](https://agentphone.to/) is an API-first voice and messaging platform built for AI agents. AgentPhone lets you provision real phone numbers, place outbound AI voice calls, send SMS and iMessage, manage conversations and contacts, and monitor usage — all through a simple REST API designed for programmatic access.
**Why AgentPhone?**
- **Agent-Native Telephony:** Purpose-built for AI agents — provision numbers, place calls, and send messages without carrier contracts or telephony plumbing.
- **Voice + Messaging in One API:** Drive outbound AI voice calls alongside SMS, MMS, and iMessage from the same account and phone numbers.
- **Conversation & Transcript Management:** Every call returns an ordered transcript; every message thread is tracked as a conversation with full history and metadata.
- **Contacts Built In:** Create, search, update, and delete contacts on the account so your agents can reference people by name instead of raw phone numbers.
- **Usage Visibility:** Inspect plan limits, current counts, and daily/monthly aggregation so workflows can stay inside guardrails.
**Using AgentPhone in Sim**
Sim's AgentPhone integration connects your agentic workflows directly to AgentPhone using an API key. With 22 operations spanning numbers, calls, conversations, contacts, and usage, you can build powerful voice and messaging automations without writing backend code.
**Key benefits of using AgentPhone in Sim:**
- **Dynamic number provisioning:** Reserve US or Canadian numbers on the fly — per agent, per customer, or per workflow — and release them when no longer needed.
- **Outbound AI voice calls:** Place calls from an agent with an optional greeting, voice override, or system prompt, and read the full transcript back as structured data once the call completes.
- **Two-way messaging:** Send SMS, MMS, or iMessage, fetch conversation history, and react to incoming iMessages — all from inside your workflow.
- **Contact and metadata management:** Keep an account-level contact list and attach custom JSON metadata to conversations so downstream blocks can branch on state.
- **Operational insight:** Pull current usage stats and daily/monthly breakdowns to monitor consumption and enforce plan limits before making the next call.
Whether you're building an outbound AI voice agent, running automated SMS follow-ups, managing two-way customer conversations, or monitoring phone usage across your organization, AgentPhone in Sim gives you direct, secure access to the full AgentPhone API — no middleware required. Simply configure your API key, select the operation you need, and let Sim handle the rest.
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Give your workflow a phone. Provision SMS- and voice-enabled numbers, send messages and tapback reactions, place outbound voice calls, manage conversations and contacts, and track usage — all through a single AgentPhone API key.
## Tools
### `agentphone_create_call`
Initiate an outbound voice call from an AgentPhone agent
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | AgentPhone API key |
| `agentId` | string | Yes | Agent that will handle the call |
| `toNumber` | string | Yes | Phone number to call in E.164 format \(e.g. +14155551234\) |
| `fromNumberId` | string | No | Phone number ID to use as caller ID. Must belong to the agent. If omitted, the agent's first assigned number is used. |
| `initialGreeting` | string | No | Optional greeting spoken when the recipient answers |
| `voice` | string | No | Voice ID override for this call \(defaults to the agent's configured voice\) |
| `systemPrompt` | string | No | When provided, uses a built-in LLM for the conversation instead of forwarding to your webhook |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Unique call identifier |
| `agentId` | string | Agent handling the call |
| `status` | string | Initial call status |
| `toNumber` | string | Destination phone number |
| `fromNumber` | string | Caller ID used for the call |
| `phoneNumberId` | string | ID of the phone number used as caller ID |
| `direction` | string | Call direction \(outbound\) |
| `startedAt` | string | ISO 8601 timestamp |
### `agentphone_create_contact`
Create a new contact in AgentPhone
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | AgentPhone API key |
| `phoneNumber` | string | Yes | Phone number in E.164 format \(e.g. +14155551234\) |
| `name` | string | Yes | Contact's full name |
| `email` | string | No | Contact's email address |
| `notes` | string | No | Freeform notes stored on the contact |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Contact ID |
| `phoneNumber` | string | Phone number in E.164 format |
| `name` | string | Contact name |
| `email` | string | Contact email address |
| `notes` | string | Freeform notes |
| `createdAt` | string | ISO 8601 creation timestamp |
| `updatedAt` | string | ISO 8601 update timestamp |
### `agentphone_create_number`
Provision a new SMS- and voice-enabled phone number
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | AgentPhone API key |
| `country` | string | No | Two-letter country code \(e.g. US, CA\). Defaults to US. |
| `areaCode` | string | No | Preferred area code \(US/CA only, e.g. "415"\). Best-effort — may be ignored if unavailable. |
| `agentId` | string | No | Optionally attach the number to an agent immediately |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Unique phone number ID |
| `phoneNumber` | string | Provisioned phone number in E.164 format |
| `country` | string | Two-letter country code |
| `status` | string | Number status \(e.g. active\) |
| `type` | string | Number type \(e.g. sms\) |
| `agentId` | string | Agent the number is attached to |
| `createdAt` | string | ISO 8601 timestamp when the number was created |
### `agentphone_delete_contact`
Delete a contact by ID
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | AgentPhone API key |
| `contactId` | string | Yes | Contact ID |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | ID of the deleted contact |
| `deleted` | boolean | Whether the contact was deleted successfully |
### `agentphone_get_call`
Fetch a call and its full transcript
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | AgentPhone API key |
| `callId` | string | Yes | ID of the call to retrieve |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Call ID |
| `agentId` | string | Agent that handled the call |
| `phoneNumberId` | string | Phone number ID |
| `phoneNumber` | string | Phone number used for the call |
| `fromNumber` | string | Caller phone number |
| `toNumber` | string | Recipient phone number |
| `direction` | string | inbound or outbound |
| `status` | string | Call status |
| `startedAt` | string | ISO 8601 timestamp |
| `endedAt` | string | ISO 8601 timestamp |
| `durationSeconds` | number | Call duration in seconds |
| `lastTranscriptSnippet` | string | Last transcript snippet |
| `recordingUrl` | string | Recording audio URL |
| `recordingAvailable` | boolean | Whether a recording is available |
| `transcripts` | array | Ordered transcript turns for the call |
| ↳ `id` | string | Transcript turn ID |
| ↳ `transcript` | string | User utterance |
| ↳ `confidence` | number | Speech recognition confidence |
| ↳ `response` | string | Agent response \(when available\) |
| ↳ `createdAt` | string | ISO 8601 timestamp |
### `agentphone_get_call_transcript`
Get the full ordered transcript for a call
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | AgentPhone API key |
| `callId` | string | Yes | ID of the call to retrieve the transcript for |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `callId` | string | Call ID |
| `transcript` | array | Ordered transcript turns for the call |
| ↳ `role` | string | Speaker role \(user or agent\) |
| ↳ `content` | string | Turn content |
| ↳ `createdAt` | string | ISO 8601 timestamp |
### `agentphone_get_contact`
Fetch a single contact by ID
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | AgentPhone API key |
| `contactId` | string | Yes | Contact ID |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Contact ID |
| `phoneNumber` | string | Phone number in E.164 format |
| `name` | string | Contact name |
| `email` | string | Contact email address |
| `notes` | string | Freeform notes |
| `createdAt` | string | ISO 8601 creation timestamp |
| `updatedAt` | string | ISO 8601 update timestamp |
### `agentphone_get_conversation`
Get a conversation along with its recent messages
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | AgentPhone API key |
| `conversationId` | string | Yes | Conversation ID |
| `messageLimit` | number | No | Number of recent messages to include \(default 50, max 100\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Conversation ID |
| `agentId` | string | Agent ID |
| `phoneNumberId` | string | Phone number ID |
| `phoneNumber` | string | Phone number |
| `participant` | string | External participant phone number |
| `lastMessageAt` | string | ISO 8601 timestamp |
| `messageCount` | number | Number of messages in the conversation |
| `metadata` | json | Custom metadata stored on the conversation |
| `createdAt` | string | ISO 8601 timestamp |
| `messages` | array | Recent messages in the conversation |
| ↳ `id` | string | Message ID |
| ↳ `body` | string | Message text |
| ↳ `fromNumber` | string | Sender phone number |
| ↳ `toNumber` | string | Recipient phone number |
| ↳ `direction` | string | inbound or outbound |
| ↳ `channel` | string | sms, mms, or imessage |
| ↳ `mediaUrl` | string | Attached media URL |
| ↳ `receivedAt` | string | ISO 8601 timestamp |
### `agentphone_get_conversation_messages`
Get paginated messages for a conversation
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | AgentPhone API key |
| `conversationId` | string | Yes | Conversation ID |
| `limit` | number | No | Number of messages to return \(default 50, max 200\) |
| `before` | string | No | Return messages received before this ISO 8601 timestamp |
| `after` | string | No | Return messages received after this ISO 8601 timestamp |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `data` | array | Messages in the conversation |
| ↳ `id` | string | Message ID |
| ↳ `body` | string | Message text |
| ↳ `fromNumber` | string | Sender phone number |
| ↳ `toNumber` | string | Recipient phone number |
| ↳ `direction` | string | inbound or outbound |
| ↳ `channel` | string | sms, mms, or imessage |
| ↳ `mediaUrl` | string | Attached media URL |
| ↳ `receivedAt` | string | ISO 8601 timestamp |
| `hasMore` | boolean | Whether more messages are available |
### `agentphone_get_number_messages`
Fetch messages received on a specific phone number
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | AgentPhone API key |
| `numberId` | string | Yes | ID of the phone number |
| `limit` | number | No | Number of messages to return \(default 50, max 200\) |
| `before` | string | No | Return messages received before this ISO 8601 timestamp |
| `after` | string | No | Return messages received after this ISO 8601 timestamp |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `data` | array | Messages received on the number |
| ↳ `id` | string | Message ID |
| ↳ `from_` | string | Sender phone number \(E.164\) |
| ↳ `to` | string | Recipient phone number \(E.164\) |
| ↳ `body` | string | Message text |
| ↳ `direction` | string | inbound or outbound |
| ↳ `channel` | string | Channel \(sms, mms, etc.\) |
| ↳ `receivedAt` | string | ISO 8601 timestamp |
| `hasMore` | boolean | Whether more messages are available |
### `agentphone_get_usage`
Retrieve current usage statistics for the AgentPhone account
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | AgentPhone API key |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `plan` | json | Plan name and limits \(name, limits: numbers/messagesPerMonth/voiceMinutesPerMonth/maxCallDurationMinutes/concurrentCalls\) |
| `numbers` | json | Phone number usage \(used, limit, remaining\) |
| `stats` | json | Usage stats: totalMessages, messagesLast24h/7d/30d, totalCalls, callsLast24h/7d/30d, totalWebhookDeliveries, successfulWebhookDeliveries, failedWebhookDeliveries |
| `periodStart` | string | Billing period start |
| `periodEnd` | string | Billing period end |
### `agentphone_get_usage_daily`
Get a daily breakdown of usage (messages, calls, webhooks) for the last N days
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | AgentPhone API key |
| `days` | number | No | Number of days to return \(1-365, default 30\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `data` | array | Daily usage entries |
| ↳ `date` | string | Day \(YYYY-MM-DD\) |
| ↳ `messages` | number | Messages that day |
| ↳ `calls` | number | Calls that day |
| ↳ `webhooks` | number | Webhook deliveries that day |
| `days` | number | Number of days returned |
### `agentphone_get_usage_monthly`
Get monthly usage aggregation (messages, calls, webhooks) for the last N months
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | AgentPhone API key |
| `months` | number | No | Number of months to return \(1-24, default 6\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `data` | array | Monthly usage entries |
| ↳ `month` | string | Month \(YYYY-MM\) |
| ↳ `messages` | number | Messages that month |
| ↳ `calls` | number | Calls that month |
| ↳ `webhooks` | number | Webhook deliveries that month |
| `months` | number | Number of months returned |
### `agentphone_list_calls`
List voice calls for this AgentPhone account
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | AgentPhone API key |
| `limit` | number | No | Number of results to return \(default 20, max 100\) |
| `offset` | number | No | Number of results to skip \(min 0\) |
| `status` | string | No | Filter by status \(completed, in-progress, failed\) |
| `direction` | string | No | Filter by direction \(inbound, outbound\) |
| `type` | string | No | Filter by call type \(pstn, web\) |
| `search` | string | No | Search by phone number \(matches fromNumber or toNumber\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `data` | array | Calls |
| ↳ `id` | string | Call ID |
| ↳ `agentId` | string | Agent that handled the call |
| ↳ `phoneNumberId` | string | Phone number ID used for the call |
| ↳ `phoneNumber` | string | Phone number used for the call |
| ↳ `fromNumber` | string | Caller phone number |
| ↳ `toNumber` | string | Recipient phone number |
| ↳ `direction` | string | inbound or outbound |
| ↳ `status` | string | Call status |
| ↳ `startedAt` | string | ISO 8601 timestamp |
| ↳ `endedAt` | string | ISO 8601 timestamp |
| ↳ `durationSeconds` | number | Call duration in seconds |
| ↳ `lastTranscriptSnippet` | string | Last transcript snippet |
| ↳ `recordingUrl` | string | Recording audio URL |
| ↳ `recordingAvailable` | boolean | Whether a recording is available |
| `hasMore` | boolean | Whether more results are available |
| `total` | number | Total number of matching calls |
### `agentphone_list_contacts`
List contacts for this AgentPhone account
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | AgentPhone API key |
| `search` | string | No | Filter by name or phone number \(case-insensitive contains\) |
| `limit` | number | No | Number of results to return \(default 50\) |
| `offset` | number | No | Number of results to skip \(min 0\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `data` | array | Contacts |
| ↳ `id` | string | Contact ID |
| ↳ `phoneNumber` | string | Phone number in E.164 format |
| ↳ `name` | string | Contact name |
| ↳ `email` | string | Contact email address |
| ↳ `notes` | string | Freeform notes |
| ↳ `createdAt` | string | ISO 8601 creation timestamp |
| ↳ `updatedAt` | string | ISO 8601 update timestamp |
| `hasMore` | boolean | Whether more results are available |
| `total` | number | Total number of contacts |
### `agentphone_list_conversations`
List conversations (message threads) for this AgentPhone account
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | AgentPhone API key |
| `limit` | number | No | Number of results to return \(default 20, max 100\) |
| `offset` | number | No | Number of results to skip \(min 0\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `data` | array | Conversations |
| ↳ `id` | string | Conversation ID |
| ↳ `agentId` | string | Agent ID |
| ↳ `phoneNumberId` | string | Phone number ID |
| ↳ `phoneNumber` | string | Phone number |
| ↳ `participant` | string | External participant phone number |
| ↳ `lastMessageAt` | string | ISO 8601 timestamp |
| ↳ `lastMessagePreview` | string | Last message preview |
| ↳ `messageCount` | number | Number of messages in the conversation |
| ↳ `metadata` | json | Custom metadata stored on the conversation |
| ↳ `createdAt` | string | ISO 8601 timestamp |
| `hasMore` | boolean | Whether more results are available |
| `total` | number | Total number of conversations |
### `agentphone_list_numbers`
List all phone numbers provisioned for this AgentPhone account
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | AgentPhone API key |
| `limit` | number | No | Number of results to return \(default 20, max 100\) |
| `offset` | number | No | Number of results to skip \(min 0\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `data` | array | Phone numbers |
| ↳ `id` | string | Phone number ID |
| ↳ `phoneNumber` | string | Phone number in E.164 format |
| ↳ `country` | string | Two-letter country code |
| ↳ `status` | string | Number status |
| ↳ `type` | string | Number type \(e.g. sms\) |
| ↳ `agentId` | string | Attached agent ID |
| ↳ `createdAt` | string | ISO 8601 creation timestamp |
| `hasMore` | boolean | Whether more results are available |
| `total` | number | Total number of phone numbers |
### `agentphone_react_to_message`
Send an iMessage tapback reaction to a message (iMessage only)
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | AgentPhone API key |
| `messageId` | string | Yes | ID of the message to react to |
| `reaction` | string | Yes | Reaction type: love, like, dislike, laugh, emphasize, or question |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Reaction ID |
| `reactionType` | string | Reaction type applied |
| `messageId` | string | ID of the message that was reacted to |
| `channel` | string | Channel \(imessage\) |
### `agentphone_release_number`
Release (delete) a phone number. This action is irreversible.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | AgentPhone API key |
| `numberId` | string | Yes | ID of the phone number to release |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | ID of the released phone number |
| `released` | boolean | Whether the number was released successfully |
### `agentphone_send_message`
Send an outbound SMS or iMessage from an AgentPhone agent
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | AgentPhone API key |
| `agentId` | string | Yes | Agent sending the message |
| `toNumber` | string | Yes | Recipient phone number in E.164 format \(e.g. +14155551234\) |
| `body` | string | Yes | Message text to send |
| `mediaUrl` | string | No | Optional URL of an image, video, or file to attach |
| `numberId` | string | No | Phone number ID to send from. If omitted, the agent's first assigned number is used. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Message ID |
| `status` | string | Delivery status |
| `channel` | string | sms, mms, or imessage |
| `fromNumber` | string | Sender phone number |
| `toNumber` | string | Recipient phone number |
### `agentphone_update_contact`
Update a contact
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | AgentPhone API key |
| `contactId` | string | Yes | Contact ID |
| `phoneNumber` | string | No | New phone number in E.164 format |
| `name` | string | No | New contact name |
| `email` | string | No | New email address |
| `notes` | string | No | New freeform notes |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Contact ID |
| `phoneNumber` | string | Phone number in E.164 format |
| `name` | string | Contact name |
| `email` | string | Contact email address |
| `notes` | string | Freeform notes |
| `createdAt` | string | ISO 8601 creation timestamp |
| `updatedAt` | string | ISO 8601 update timestamp |
### `agentphone_update_conversation`
Update conversation metadata (stored state). Pass null to clear existing metadata.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | AgentPhone API key |
| `conversationId` | string | Yes | Conversation ID |
| `metadata` | json | No | Custom key-value metadata to store on the conversation. Pass null to clear existing metadata. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Conversation ID |
| `agentId` | string | Agent ID |
| `phoneNumberId` | string | Phone number ID |
| `phoneNumber` | string | Phone number |
| `participant` | string | External participant phone number |
| `lastMessageAt` | string | ISO 8601 timestamp |
| `messageCount` | number | Number of messages |
| `metadata` | json | Custom metadata stored on the conversation |
| `createdAt` | string | ISO 8601 timestamp |
| `messages` | array | Messages in the conversation |
| ↳ `id` | string | Message ID |
| ↳ `body` | string | Message body |
| ↳ `fromNumber` | string | Sender phone number |
| ↳ `toNumber` | string | Recipient phone number |
| ↳ `direction` | string | inbound or outbound |
| ↳ `channel` | string | Channel \(sms, mms, etc.\) |
| ↳ `mediaUrl` | string | Media URL if any |
| ↳ `receivedAt` | string | ISO 8601 timestamp |

View File

@@ -38,7 +38,7 @@ Integrate Ashby into the workflow. Manage candidates (list, get, create, update,
### `ashby_add_candidate_tag`
Adds a tag to a candidate in Ashby.
Adds a tag to a candidate in Ashby and returns the updated candidate.
#### Input
@@ -52,7 +52,37 @@ Adds a tag to a candidate in Ashby.
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the tag was successfully added |
| `candidates` | json | List of candidates with rich fields \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], linkedInUrl, githubUrl, profileUrl, position, company, school, timezone, location with locationComponents\[\], tags\[\], applicationIds\[\], customFields\[\], resumeFileHandle, fileHandles\[\], source with sourceType, creditedToUser, fraudStatus, createdAt, updatedAt\) |
| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion, compensation with compensationTiers\[\]\) |
| `applications` | json | List of applications \(id, status, customFields\[\], candidate summary, currentInterviewStage, source with sourceType, archiveReason with customFields\[\], archivedAt, job summary, creditedToUser, hiringTeam\[\], appliedViaJobPostingId, submitterClientIp, submitterUserAgent, createdAt, updatedAt\) |
| `notes` | json | List of notes \(id, content, author, isPrivate, createdAt\) |
| `offers` | json | List of offers \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion with id/startDate/salary/createdAt/openingId/customFields\[\]/fileHandles\[\]/author/approvalStatus\) |
| `archiveReasons` | json | List of archive reasons \(id, text, reasonType \[RejectedByCandidate/RejectedByOrg/Other\], isArchived\) |
| `sources` | json | List of sources \(id, title, isArchived, sourceType \{id, title, isArchived\}\) |
| `customFields` | json | List of custom field definitions \(id, title, isPrivate, fieldType, objectType, isArchived, isRequired, selectableValues\[\] \{label, value, isArchived\}\) |
| `departments` | json | List of departments \(id, name, externalName, isArchived, parentId, createdAt, updatedAt\) |
| `locations` | json | List of locations \(id, name, externalName, isArchived, isRemote, workplaceType, parentLocationId, type, address with addressCountry/Region/Locality/postalCode/streetAddress\) |
| `jobPostings` | json | List of job postings \(id, title, jobId, departmentName, teamName, locationName, locationIds, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensationTierSummary, shouldDisplayCompensationOnJobBoard, updatedAt\) |
| `openings` | json | List of openings \(id, openedAt, closedAt, isArchived, archivedAt, closeReasonId, openingState, latestVersion with identifier/description/authorId/createdAt/teamId/jobIds\[\]/targetHireDate/targetStartDate/isBackfill/employmentType/locationIds\[\]/hiringTeam\[\]/customFields\[\]\) |
| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt, managerId\) |
| `interviewSchedules` | json | List of interview schedules \(id, applicationId, interviewStageId, interviewEvents\[\] with interviewerUserIds/startTime/endTime/feedbackLink/location/meetingLink/hasSubmittedFeedback, status, scheduledBy, createdAt, updatedAt\) |
| `tags` | json | List of candidate tags \(id, title, isArchived\) |
| `id` | string | Resource UUID |
| `name` | string | Resource name |
| `title` | string | Job title or job posting title |
| `status` | string | Status |
| `candidate` | json | Candidate details \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], customFields\[\], source, creditedToUser, createdAt, updatedAt\) |
| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], compensation, createdAt, updatedAt\) |
| `application` | json | Application details \(id, status, customFields\[\], candidate, currentInterviewStage, source, archiveReason, job, hiringTeam\[\], createdAt, updatedAt\) |
| `offer` | json | Offer details \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion\) |
| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, linkedData, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt\) |
| `content` | string | Note content |
| `author` | json | Note author \(id, firstName, lastName, email, globalRole, isEnabled\) |
| `isPrivate` | boolean | Whether the note is private |
| `createdAt` | string | ISO 8601 creation timestamp |
| `moreDataAvailable` | boolean | Whether more pages exist |
| `nextCursor` | string | Pagination cursor for next page |
| `syncToken` | string | Sync token for incremental updates |
### `ashby_change_application_stage`
@@ -71,8 +101,37 @@ Moves an application to a different interview stage. Requires an archive reason
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `applicationId` | string | Application UUID |
| `stageId` | string | New interview stage UUID |
| `candidates` | json | List of candidates with rich fields \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], linkedInUrl, githubUrl, profileUrl, position, company, school, timezone, location with locationComponents\[\], tags\[\], applicationIds\[\], customFields\[\], resumeFileHandle, fileHandles\[\], source with sourceType, creditedToUser, fraudStatus, createdAt, updatedAt\) |
| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion, compensation with compensationTiers\[\]\) |
| `applications` | json | List of applications \(id, status, customFields\[\], candidate summary, currentInterviewStage, source with sourceType, archiveReason with customFields\[\], archivedAt, job summary, creditedToUser, hiringTeam\[\], appliedViaJobPostingId, submitterClientIp, submitterUserAgent, createdAt, updatedAt\) |
| `notes` | json | List of notes \(id, content, author, isPrivate, createdAt\) |
| `offers` | json | List of offers \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion with id/startDate/salary/createdAt/openingId/customFields\[\]/fileHandles\[\]/author/approvalStatus\) |
| `archiveReasons` | json | List of archive reasons \(id, text, reasonType \[RejectedByCandidate/RejectedByOrg/Other\], isArchived\) |
| `sources` | json | List of sources \(id, title, isArchived, sourceType \{id, title, isArchived\}\) |
| `customFields` | json | List of custom field definitions \(id, title, isPrivate, fieldType, objectType, isArchived, isRequired, selectableValues\[\] \{label, value, isArchived\}\) |
| `departments` | json | List of departments \(id, name, externalName, isArchived, parentId, createdAt, updatedAt\) |
| `locations` | json | List of locations \(id, name, externalName, isArchived, isRemote, workplaceType, parentLocationId, type, address with addressCountry/Region/Locality/postalCode/streetAddress\) |
| `jobPostings` | json | List of job postings \(id, title, jobId, departmentName, teamName, locationName, locationIds, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensationTierSummary, shouldDisplayCompensationOnJobBoard, updatedAt\) |
| `openings` | json | List of openings \(id, openedAt, closedAt, isArchived, archivedAt, closeReasonId, openingState, latestVersion with identifier/description/authorId/createdAt/teamId/jobIds\[\]/targetHireDate/targetStartDate/isBackfill/employmentType/locationIds\[\]/hiringTeam\[\]/customFields\[\]\) |
| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt, managerId\) |
| `interviewSchedules` | json | List of interview schedules \(id, applicationId, interviewStageId, interviewEvents\[\] with interviewerUserIds/startTime/endTime/feedbackLink/location/meetingLink/hasSubmittedFeedback, status, scheduledBy, createdAt, updatedAt\) |
| `tags` | json | List of candidate tags \(id, title, isArchived\) |
| `id` | string | Resource UUID |
| `name` | string | Resource name |
| `title` | string | Job title or job posting title |
| `status` | string | Status |
| `candidate` | json | Candidate details \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], customFields\[\], source, creditedToUser, createdAt, updatedAt\) |
| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], compensation, createdAt, updatedAt\) |
| `application` | json | Application details \(id, status, customFields\[\], candidate, currentInterviewStage, source, archiveReason, job, hiringTeam\[\], createdAt, updatedAt\) |
| `offer` | json | Offer details \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion\) |
| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, linkedData, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt\) |
| `content` | string | Note content |
| `author` | json | Note author \(id, firstName, lastName, email, globalRole, isEnabled\) |
| `isPrivate` | boolean | Whether the note is private |
| `createdAt` | string | ISO 8601 creation timestamp |
| `moreDataAvailable` | boolean | Whether more pages exist |
| `nextCursor` | string | Pagination cursor for next page |
| `syncToken` | string | Sync token for incremental updates |
### `ashby_create_application`
@@ -95,7 +154,37 @@ Creates a new application for a candidate on a job. Optionally specify interview
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `applicationId` | string | Created application UUID |
| `candidates` | json | List of candidates with rich fields \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], linkedInUrl, githubUrl, profileUrl, position, company, school, timezone, location with locationComponents\[\], tags\[\], applicationIds\[\], customFields\[\], resumeFileHandle, fileHandles\[\], source with sourceType, creditedToUser, fraudStatus, createdAt, updatedAt\) |
| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion, compensation with compensationTiers\[\]\) |
| `applications` | json | List of applications \(id, status, customFields\[\], candidate summary, currentInterviewStage, source with sourceType, archiveReason with customFields\[\], archivedAt, job summary, creditedToUser, hiringTeam\[\], appliedViaJobPostingId, submitterClientIp, submitterUserAgent, createdAt, updatedAt\) |
| `notes` | json | List of notes \(id, content, author, isPrivate, createdAt\) |
| `offers` | json | List of offers \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion with id/startDate/salary/createdAt/openingId/customFields\[\]/fileHandles\[\]/author/approvalStatus\) |
| `archiveReasons` | json | List of archive reasons \(id, text, reasonType \[RejectedByCandidate/RejectedByOrg/Other\], isArchived\) |
| `sources` | json | List of sources \(id, title, isArchived, sourceType \{id, title, isArchived\}\) |
| `customFields` | json | List of custom field definitions \(id, title, isPrivate, fieldType, objectType, isArchived, isRequired, selectableValues\[\] \{label, value, isArchived\}\) |
| `departments` | json | List of departments \(id, name, externalName, isArchived, parentId, createdAt, updatedAt\) |
| `locations` | json | List of locations \(id, name, externalName, isArchived, isRemote, workplaceType, parentLocationId, type, address with addressCountry/Region/Locality/postalCode/streetAddress\) |
| `jobPostings` | json | List of job postings \(id, title, jobId, departmentName, teamName, locationName, locationIds, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensationTierSummary, shouldDisplayCompensationOnJobBoard, updatedAt\) |
| `openings` | json | List of openings \(id, openedAt, closedAt, isArchived, archivedAt, closeReasonId, openingState, latestVersion with identifier/description/authorId/createdAt/teamId/jobIds\[\]/targetHireDate/targetStartDate/isBackfill/employmentType/locationIds\[\]/hiringTeam\[\]/customFields\[\]\) |
| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt, managerId\) |
| `interviewSchedules` | json | List of interview schedules \(id, applicationId, interviewStageId, interviewEvents\[\] with interviewerUserIds/startTime/endTime/feedbackLink/location/meetingLink/hasSubmittedFeedback, status, scheduledBy, createdAt, updatedAt\) |
| `tags` | json | List of candidate tags \(id, title, isArchived\) |
| `id` | string | Resource UUID |
| `name` | string | Resource name |
| `title` | string | Job title or job posting title |
| `status` | string | Status |
| `candidate` | json | Candidate details \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], customFields\[\], source, creditedToUser, createdAt, updatedAt\) |
| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], compensation, createdAt, updatedAt\) |
| `application` | json | Application details \(id, status, customFields\[\], candidate, currentInterviewStage, source, archiveReason, job, hiringTeam\[\], createdAt, updatedAt\) |
| `offer` | json | Offer details \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion\) |
| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, linkedData, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt\) |
| `content` | string | Note content |
| `author` | json | Note author \(id, firstName, lastName, email, globalRole, isEnabled\) |
| `isPrivate` | boolean | Whether the note is private |
| `createdAt` | string | ISO 8601 creation timestamp |
| `moreDataAvailable` | boolean | Whether more pages exist |
| `nextCursor` | string | Pagination cursor for next page |
| `syncToken` | string | Sync token for incremental updates |
### `ashby_create_candidate`
@@ -107,7 +196,7 @@ Creates a new candidate record in Ashby.
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Ashby API Key |
| `name` | string | Yes | The candidate full name |
| `email` | string | Yes | Primary email address for the candidate |
| `email` | string | No | Primary email address for the candidate |
| `phoneNumber` | string | No | Primary phone number for the candidate |
| `linkedInUrl` | string | No | LinkedIn profile URL |
| `githubUrl` | string | No | GitHub profile URL |
@@ -117,17 +206,37 @@ Creates a new candidate record in Ashby.
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Created candidate UUID |
| `name` | string | Full name |
| `primaryEmailAddress` | object | Primary email contact info |
| ↳ `value` | string | Email address |
| ↳ `type` | string | Contact type \(Personal, Work, Other\) |
| ↳ `isPrimary` | boolean | Whether this is the primary email |
| `primaryPhoneNumber` | object | Primary phone contact info |
| ↳ `value` | string | Phone number |
| ↳ `type` | string | Contact type \(Personal, Work, Other\) |
| ↳ `isPrimary` | boolean | Whether this is the primary phone |
| `candidates` | json | List of candidates with rich fields \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], linkedInUrl, githubUrl, profileUrl, position, company, school, timezone, location with locationComponents\[\], tags\[\], applicationIds\[\], customFields\[\], resumeFileHandle, fileHandles\[\], source with sourceType, creditedToUser, fraudStatus, createdAt, updatedAt\) |
| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion, compensation with compensationTiers\[\]\) |
| `applications` | json | List of applications \(id, status, customFields\[\], candidate summary, currentInterviewStage, source with sourceType, archiveReason with customFields\[\], archivedAt, job summary, creditedToUser, hiringTeam\[\], appliedViaJobPostingId, submitterClientIp, submitterUserAgent, createdAt, updatedAt\) |
| `notes` | json | List of notes \(id, content, author, isPrivate, createdAt\) |
| `offers` | json | List of offers \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion with id/startDate/salary/createdAt/openingId/customFields\[\]/fileHandles\[\]/author/approvalStatus\) |
| `archiveReasons` | json | List of archive reasons \(id, text, reasonType \[RejectedByCandidate/RejectedByOrg/Other\], isArchived\) |
| `sources` | json | List of sources \(id, title, isArchived, sourceType \{id, title, isArchived\}\) |
| `customFields` | json | List of custom field definitions \(id, title, isPrivate, fieldType, objectType, isArchived, isRequired, selectableValues\[\] \{label, value, isArchived\}\) |
| `departments` | json | List of departments \(id, name, externalName, isArchived, parentId, createdAt, updatedAt\) |
| `locations` | json | List of locations \(id, name, externalName, isArchived, isRemote, workplaceType, parentLocationId, type, address with addressCountry/Region/Locality/postalCode/streetAddress\) |
| `jobPostings` | json | List of job postings \(id, title, jobId, departmentName, teamName, locationName, locationIds, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensationTierSummary, shouldDisplayCompensationOnJobBoard, updatedAt\) |
| `openings` | json | List of openings \(id, openedAt, closedAt, isArchived, archivedAt, closeReasonId, openingState, latestVersion with identifier/description/authorId/createdAt/teamId/jobIds\[\]/targetHireDate/targetStartDate/isBackfill/employmentType/locationIds\[\]/hiringTeam\[\]/customFields\[\]\) |
| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt, managerId\) |
| `interviewSchedules` | json | List of interview schedules \(id, applicationId, interviewStageId, interviewEvents\[\] with interviewerUserIds/startTime/endTime/feedbackLink/location/meetingLink/hasSubmittedFeedback, status, scheduledBy, createdAt, updatedAt\) |
| `tags` | json | List of candidate tags \(id, title, isArchived\) |
| `id` | string | Resource UUID |
| `name` | string | Resource name |
| `title` | string | Job title or job posting title |
| `status` | string | Status |
| `candidate` | json | Candidate details \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], customFields\[\], source, creditedToUser, createdAt, updatedAt\) |
| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], compensation, createdAt, updatedAt\) |
| `application` | json | Application details \(id, status, customFields\[\], candidate, currentInterviewStage, source, archiveReason, job, hiringTeam\[\], createdAt, updatedAt\) |
| `offer` | json | Offer details \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion\) |
| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, linkedData, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt\) |
| `content` | string | Note content |
| `author` | json | Note author \(id, firstName, lastName, email, globalRole, isEnabled\) |
| `isPrivate` | boolean | Whether the note is private |
| `createdAt` | string | ISO 8601 creation timestamp |
| `moreDataAvailable` | boolean | Whether more pages exist |
| `nextCursor` | string | Pagination cursor for next page |
| `syncToken` | string | Sync token for incremental updates |
### `ashby_create_note`
@@ -147,7 +256,15 @@ Creates a note on a candidate in Ashby. Supports plain text and HTML content (bo
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `noteId` | string | Created note UUID |
| `id` | string | Created note UUID |
| `createdAt` | string | ISO 8601 creation timestamp |
| `isPrivate` | boolean | Whether the note is private |
| `content` | string | Note content |
| `author` | object | Author of the note |
| ↳ `id` | string | Author user UUID |
| ↳ `firstName` | string | Author first name |
| ↳ `lastName` | string | Author last name |
| ↳ `email` | string | Author email |
### `ashby_get_application`
@@ -164,28 +281,37 @@ Retrieves full details about a single application by its ID.
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Application UUID |
| `status` | string | Application status \(Active, Hired, Archived, Lead\) |
| `candidate` | object | Associated candidate |
| ↳ `id` | string | Candidate UUID |
| ↳ `name` | string | Candidate name |
| `job` | object | Associated job |
| ↳ `id` | string | Job UUID |
| ↳ `title` | string | Job title |
| `currentInterviewStage` | object | Current interview stage |
| ↳ `id` | string | Stage UUID |
| ↳ `title` | string | Stage title |
| ↳ `type` | string | Stage type |
| `source` | object | Application source |
| ↳ `id` | string | Source UUID |
| ↳ `title` | string | Source title |
| `archiveReason` | object | Reason for archival |
| ↳ `id` | string | Reason UUID |
| ↳ `text` | string | Reason text |
| ↳ `reasonType` | string | Reason type |
| `archivedAt` | string | ISO 8601 archive timestamp |
| `candidates` | json | List of candidates with rich fields \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], linkedInUrl, githubUrl, profileUrl, position, company, school, timezone, location with locationComponents\[\], tags\[\], applicationIds\[\], customFields\[\], resumeFileHandle, fileHandles\[\], source with sourceType, creditedToUser, fraudStatus, createdAt, updatedAt\) |
| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion, compensation with compensationTiers\[\]\) |
| `applications` | json | List of applications \(id, status, customFields\[\], candidate summary, currentInterviewStage, source with sourceType, archiveReason with customFields\[\], archivedAt, job summary, creditedToUser, hiringTeam\[\], appliedViaJobPostingId, submitterClientIp, submitterUserAgent, createdAt, updatedAt\) |
| `notes` | json | List of notes \(id, content, author, isPrivate, createdAt\) |
| `offers` | json | List of offers \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion with id/startDate/salary/createdAt/openingId/customFields\[\]/fileHandles\[\]/author/approvalStatus\) |
| `archiveReasons` | json | List of archive reasons \(id, text, reasonType \[RejectedByCandidate/RejectedByOrg/Other\], isArchived\) |
| `sources` | json | List of sources \(id, title, isArchived, sourceType \{id, title, isArchived\}\) |
| `customFields` | json | List of custom field definitions \(id, title, isPrivate, fieldType, objectType, isArchived, isRequired, selectableValues\[\] \{label, value, isArchived\}\) |
| `departments` | json | List of departments \(id, name, externalName, isArchived, parentId, createdAt, updatedAt\) |
| `locations` | json | List of locations \(id, name, externalName, isArchived, isRemote, workplaceType, parentLocationId, type, address with addressCountry/Region/Locality/postalCode/streetAddress\) |
| `jobPostings` | json | List of job postings \(id, title, jobId, departmentName, teamName, locationName, locationIds, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensationTierSummary, shouldDisplayCompensationOnJobBoard, updatedAt\) |
| `openings` | json | List of openings \(id, openedAt, closedAt, isArchived, archivedAt, closeReasonId, openingState, latestVersion with identifier/description/authorId/createdAt/teamId/jobIds\[\]/targetHireDate/targetStartDate/isBackfill/employmentType/locationIds\[\]/hiringTeam\[\]/customFields\[\]\) |
| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt, managerId\) |
| `interviewSchedules` | json | List of interview schedules \(id, applicationId, interviewStageId, interviewEvents\[\] with interviewerUserIds/startTime/endTime/feedbackLink/location/meetingLink/hasSubmittedFeedback, status, scheduledBy, createdAt, updatedAt\) |
| `tags` | json | List of candidate tags \(id, title, isArchived\) |
| `id` | string | Resource UUID |
| `name` | string | Resource name |
| `title` | string | Job title or job posting title |
| `status` | string | Status |
| `candidate` | json | Candidate details \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], customFields\[\], source, creditedToUser, createdAt, updatedAt\) |
| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], compensation, createdAt, updatedAt\) |
| `application` | json | Application details \(id, status, customFields\[\], candidate, currentInterviewStage, source, archiveReason, job, hiringTeam\[\], createdAt, updatedAt\) |
| `offer` | json | Offer details \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion\) |
| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, linkedData, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt\) |
| `content` | string | Note content |
| `author` | json | Note author \(id, firstName, lastName, email, globalRole, isEnabled\) |
| `isPrivate` | boolean | Whether the note is private |
| `createdAt` | string | ISO 8601 creation timestamp |
| `updatedAt` | string | ISO 8601 last update timestamp |
| `moreDataAvailable` | boolean | Whether more pages exist |
| `nextCursor` | string | Pagination cursor for next page |
| `syncToken` | string | Sync token for incremental updates |
### `ashby_get_candidate`
@@ -202,27 +328,37 @@ Retrieves full details about a single candidate by their ID.
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Candidate UUID |
| `name` | string | Full name |
| `primaryEmailAddress` | object | Primary email contact info |
| ↳ `value` | string | Email address |
| ↳ `type` | string | Contact type \(Personal, Work, Other\) |
| ↳ `isPrimary` | boolean | Whether this is the primary email |
| `primaryPhoneNumber` | object | Primary phone contact info |
| ↳ `value` | string | Phone number |
| ↳ `type` | string | Contact type \(Personal, Work, Other\) |
| ↳ `isPrimary` | boolean | Whether this is the primary phone |
| `profileUrl` | string | URL to the candidate Ashby profile |
| `position` | string | Current position or title |
| `company` | string | Current company |
| `linkedInUrl` | string | LinkedIn profile URL |
| `githubUrl` | string | GitHub profile URL |
| `tags` | array | Tags applied to the candidate |
| ↳ `id` | string | Tag UUID |
| `title` | string | Tag title |
| `applicationIds` | array | IDs of associated applications |
| `candidates` | json | List of candidates with rich fields \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], linkedInUrl, githubUrl, profileUrl, position, company, school, timezone, location with locationComponents\[\], tags\[\], applicationIds\[\], customFields\[\], resumeFileHandle, fileHandles\[\], source with sourceType, creditedToUser, fraudStatus, createdAt, updatedAt\) |
| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion, compensation with compensationTiers\[\]\) |
| `applications` | json | List of applications \(id, status, customFields\[\], candidate summary, currentInterviewStage, source with sourceType, archiveReason with customFields\[\], archivedAt, job summary, creditedToUser, hiringTeam\[\], appliedViaJobPostingId, submitterClientIp, submitterUserAgent, createdAt, updatedAt\) |
| `notes` | json | List of notes \(id, content, author, isPrivate, createdAt\) |
| `offers` | json | List of offers \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion with id/startDate/salary/createdAt/openingId/customFields\[\]/fileHandles\[\]/author/approvalStatus\) |
| `archiveReasons` | json | List of archive reasons \(id, text, reasonType \[RejectedByCandidate/RejectedByOrg/Other\], isArchived\) |
| `sources` | json | List of sources \(id, title, isArchived, sourceType \{id, title, isArchived\}\) |
| `customFields` | json | List of custom field definitions \(id, title, isPrivate, fieldType, objectType, isArchived, isRequired, selectableValues\[\] \{label, value, isArchived\}\) |
| `departments` | json | List of departments \(id, name, externalName, isArchived, parentId, createdAt, updatedAt\) |
| `locations` | json | List of locations \(id, name, externalName, isArchived, isRemote, workplaceType, parentLocationId, type, address with addressCountry/Region/Locality/postalCode/streetAddress\) |
| `jobPostings` | json | List of job postings \(id, title, jobId, departmentName, teamName, locationName, locationIds, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensationTierSummary, shouldDisplayCompensationOnJobBoard, updatedAt\) |
| `openings` | json | List of openings \(id, openedAt, closedAt, isArchived, archivedAt, closeReasonId, openingState, latestVersion with identifier/description/authorId/createdAt/teamId/jobIds\[\]/targetHireDate/targetStartDate/isBackfill/employmentType/locationIds\[\]/hiringTeam\[\]/customFields\[\]\) |
| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt, managerId\) |
| `interviewSchedules` | json | List of interview schedules \(id, applicationId, interviewStageId, interviewEvents\[\] with interviewerUserIds/startTime/endTime/feedbackLink/location/meetingLink/hasSubmittedFeedback, status, scheduledBy, createdAt, updatedAt\) |
| `tags` | json | List of candidate tags \(id, title, isArchived\) |
| `id` | string | Resource UUID |
| `name` | string | Resource name |
| `title` | string | Job title or job posting title |
| `status` | string | Status |
| `candidate` | json | Candidate details \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], customFields\[\], source, creditedToUser, createdAt, updatedAt\) |
| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], compensation, createdAt, updatedAt\) |
| `application` | json | Application details \(id, status, customFields\[\], candidate, currentInterviewStage, source, archiveReason, job, hiringTeam\[\], createdAt, updatedAt\) |
| `offer` | json | Offer details \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion\) |
| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, linkedData, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt\) |
| `content` | string | Note content |
| `author` | json | Note author \(id, firstName, lastName, email, globalRole, isEnabled\) |
| `isPrivate` | boolean | Whether the note is private |
| `createdAt` | string | ISO 8601 creation timestamp |
| `updatedAt` | string | ISO 8601 last update timestamp |
| `moreDataAvailable` | boolean | Whether more pages exist |
| `nextCursor` | string | Pagination cursor for next page |
| `syncToken` | string | Sync token for incremental updates |
### `ashby_get_job`
@@ -239,16 +375,37 @@ Retrieves full details about a single job by its ID.
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Job UUID |
| `title` | string | Job title |
| `status` | string | Job status \(Open, Closed, Draft, Archived\) |
| `employmentType` | string | Employment type \(FullTime, PartTime, Intern, Contract, Temporary\) |
| `departmentId` | string | Department UUID |
| `locationId` | string | Location UUID |
| `descriptionPlain` | string | Job description in plain text |
| `isArchived` | boolean | Whether the job is archived |
| `candidates` | json | List of candidates with rich fields \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], linkedInUrl, githubUrl, profileUrl, position, company, school, timezone, location with locationComponents\[\], tags\[\], applicationIds\[\], customFields\[\], resumeFileHandle, fileHandles\[\], source with sourceType, creditedToUser, fraudStatus, createdAt, updatedAt\) |
| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion, compensation with compensationTiers\[\]\) |
| `applications` | json | List of applications \(id, status, customFields\[\], candidate summary, currentInterviewStage, source with sourceType, archiveReason with customFields\[\], archivedAt, job summary, creditedToUser, hiringTeam\[\], appliedViaJobPostingId, submitterClientIp, submitterUserAgent, createdAt, updatedAt\) |
| `notes` | json | List of notes \(id, content, author, isPrivate, createdAt\) |
| `offers` | json | List of offers \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion with id/startDate/salary/createdAt/openingId/customFields\[\]/fileHandles\[\]/author/approvalStatus\) |
| `archiveReasons` | json | List of archive reasons \(id, text, reasonType \[RejectedByCandidate/RejectedByOrg/Other\], isArchived\) |
| `sources` | json | List of sources \(id, title, isArchived, sourceType \{id, title, isArchived\}\) |
| `customFields` | json | List of custom field definitions \(id, title, isPrivate, fieldType, objectType, isArchived, isRequired, selectableValues\[\] \{label, value, isArchived\}\) |
| `departments` | json | List of departments \(id, name, externalName, isArchived, parentId, createdAt, updatedAt\) |
| `locations` | json | List of locations \(id, name, externalName, isArchived, isRemote, workplaceType, parentLocationId, type, address with addressCountry/Region/Locality/postalCode/streetAddress\) |
| `jobPostings` | json | List of job postings \(id, title, jobId, departmentName, teamName, locationName, locationIds, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensationTierSummary, shouldDisplayCompensationOnJobBoard, updatedAt\) |
| `openings` | json | List of openings \(id, openedAt, closedAt, isArchived, archivedAt, closeReasonId, openingState, latestVersion with identifier/description/authorId/createdAt/teamId/jobIds\[\]/targetHireDate/targetStartDate/isBackfill/employmentType/locationIds\[\]/hiringTeam\[\]/customFields\[\]\) |
| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt, managerId\) |
| `interviewSchedules` | json | List of interview schedules \(id, applicationId, interviewStageId, interviewEvents\[\] with interviewerUserIds/startTime/endTime/feedbackLink/location/meetingLink/hasSubmittedFeedback, status, scheduledBy, createdAt, updatedAt\) |
| `tags` | json | List of candidate tags \(id, title, isArchived\) |
| `id` | string | Resource UUID |
| `name` | string | Resource name |
| `title` | string | Job title or job posting title |
| `status` | string | Status |
| `candidate` | json | Candidate details \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], customFields\[\], source, creditedToUser, createdAt, updatedAt\) |
| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], compensation, createdAt, updatedAt\) |
| `application` | json | Application details \(id, status, customFields\[\], candidate, currentInterviewStage, source, archiveReason, job, hiringTeam\[\], createdAt, updatedAt\) |
| `offer` | json | Offer details \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion\) |
| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, linkedData, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt\) |
| `content` | string | Note content |
| `author` | json | Note author \(id, firstName, lastName, email, globalRole, isEnabled\) |
| `isPrivate` | boolean | Whether the note is private |
| `createdAt` | string | ISO 8601 creation timestamp |
| `updatedAt` | string | ISO 8601 last update timestamp |
| `moreDataAvailable` | boolean | Whether more pages exist |
| `nextCursor` | string | Pagination cursor for next page |
| `syncToken` | string | Sync token for incremental updates |
### `ashby_get_job_posting`
@@ -260,6 +417,8 @@ Retrieves full details about a single job posting by its ID.
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Ashby API Key |
| `jobPostingId` | string | Yes | The UUID of the job posting to fetch |
| `expandApplicationFormDefinition` | boolean | No | Include application form definition in the response |
| `expandSurveyFormDefinitions` | boolean | No | Include survey form definitions in the response |
#### Output
@@ -267,14 +426,56 @@ Retrieves full details about a single job posting by its ID.
| --------- | ---- | ----------- |
| `id` | string | Job posting UUID |
| `title` | string | Job posting title |
| `jobId` | string | Associated job UUID |
| `locationName` | string | Location name |
| `descriptionPlain` | string | Full description in plain text |
| `descriptionHtml` | string | Full description in HTML |
| `descriptionSocial` | string | Shortened description for social sharing \(max 200 chars\) |
| `descriptionParts` | object | Description broken into opening, body, and closing sections |
| ↳ `descriptionOpening` | object | Opening \(from Job Boards theme settings\) |
| ↳ `html` | string | HTML content |
| ↳ `plain` | string | Plain text content |
| ↳ `descriptionBody` | object | Main description body |
| ↳ `html` | string | HTML content |
| ↳ `plain` | string | Plain text content |
| ↳ `descriptionClosing` | object | Closing \(from Job Boards theme settings\) |
| ↳ `html` | string | HTML content |
| ↳ `plain` | string | Plain text content |
| `departmentName` | string | Department name |
| `employmentType` | string | Employment type \(e.g. FullTime, PartTime, Contract\) |
| `descriptionPlain` | string | Job posting description in plain text |
| `isListed` | boolean | Whether the posting is publicly listed |
| `teamName` | string | Team name |
| `teamNameHierarchy` | array | Hierarchy of team names from root to team |
| `jobId` | string | Associated job UUID |
| `locationName` | string | Primary location name |
| `locationIds` | object | Primary and secondary location UUIDs |
| ↳ `primaryLocationId` | string | Primary location UUID |
| ↳ `secondaryLocationIds` | array | Secondary location UUIDs |
| `address` | object | Postal address of the posting location |
| ↳ `postalAddress` | object | Structured postal address |
| ↳ `addressCountry` | string | Country |
| ↳ `addressRegion` | string | State or region |
| ↳ `addressLocality` | string | City or locality |
| ↳ `postalCode` | string | Postal code |
| ↳ `streetAddress` | string | Street address |
| `isRemote` | boolean | Whether the posting is remote |
| `workplaceType` | string | Workplace type \(OnSite, Remote, Hybrid\) |
| `employmentType` | string | Employment type \(FullTime, PartTime, Intern, Contract, Temporary\) |
| `isListed` | boolean | Whether publicly listed on the job board |
| `suppressDescriptionOpening` | boolean | Whether the theme opening is hidden on this posting |
| `suppressDescriptionClosing` | boolean | Whether the theme closing is hidden on this posting |
| `publishedDate` | string | ISO 8601 published date |
| `applicationDeadline` | string | ISO 8601 application deadline |
| `externalLink` | string | External link to the job posting |
| `applyLink` | string | Direct apply link |
| `compensation` | object | Compensation details for the posting |
| ↳ `compensationTierSummary` | string | Human-readable tier summary |
| ↳ `summaryComponents` | array | Structured compensation components |
| ↳ `summary` | string | Component summary |
| ↳ `compensationTypeLabel` | string | Component type label \(Salary, Commission, Bonus, Equity, etc.\) |
| ↳ `interval` | string | Payment interval \(e.g. annual, hourly\) |
| ↳ `currencyCode` | string | ISO 4217 currency code |
| ↳ `minValue` | number | Minimum value |
| ↳ `maxValue` | number | Maximum value |
| ↳ `shouldDisplayCompensationOnJobBoard` | boolean | Whether compensation is shown on the job board |
| `applicationLimitCalloutHtml` | string | HTML callout shown when application limit is reached |
| `updatedAt` | string | ISO 8601 last update timestamp |
### `ashby_get_offer`
@@ -291,20 +492,41 @@ Retrieves full details about a single offer by its ID.
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Offer UUID |
| `offerStatus` | string | Offer status \(e.g. WaitingOnCandidateResponse, CandidateAccepted\) |
| `acceptanceStatus` | string | Acceptance status \(e.g. Accepted, Declined, Pending\) |
| `applicationId` | string | Associated application UUID |
| `startDate` | string | Offer start date |
| `salary` | object | Salary details |
| ↳ `currencyCode` | string | ISO 4217 currency code |
| ↳ `value` | number | Salary amount |
| `openingId` | string | Associated opening UUID |
| `createdAt` | string | ISO 8601 creation timestamp \(from latest version\) |
| `candidates` | json | List of candidates with rich fields \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], linkedInUrl, githubUrl, profileUrl, position, company, school, timezone, location with locationComponents\[\], tags\[\], applicationIds\[\], customFields\[\], resumeFileHandle, fileHandles\[\], source with sourceType, creditedToUser, fraudStatus, createdAt, updatedAt\) |
| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion, compensation with compensationTiers\[\]\) |
| `applications` | json | List of applications \(id, status, customFields\[\], candidate summary, currentInterviewStage, source with sourceType, archiveReason with customFields\[\], archivedAt, job summary, creditedToUser, hiringTeam\[\], appliedViaJobPostingId, submitterClientIp, submitterUserAgent, createdAt, updatedAt\) |
| `notes` | json | List of notes \(id, content, author, isPrivate, createdAt\) |
| `offers` | json | List of offers \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion with id/startDate/salary/createdAt/openingId/customFields\[\]/fileHandles\[\]/author/approvalStatus\) |
| `archiveReasons` | json | List of archive reasons \(id, text, reasonType \[RejectedByCandidate/RejectedByOrg/Other\], isArchived\) |
| `sources` | json | List of sources \(id, title, isArchived, sourceType \{id, title, isArchived\}\) |
| `customFields` | json | List of custom field definitions \(id, title, isPrivate, fieldType, objectType, isArchived, isRequired, selectableValues\[\] \{label, value, isArchived\}\) |
| `departments` | json | List of departments \(id, name, externalName, isArchived, parentId, createdAt, updatedAt\) |
| `locations` | json | List of locations \(id, name, externalName, isArchived, isRemote, workplaceType, parentLocationId, type, address with addressCountry/Region/Locality/postalCode/streetAddress\) |
| `jobPostings` | json | List of job postings \(id, title, jobId, departmentName, teamName, locationName, locationIds, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensationTierSummary, shouldDisplayCompensationOnJobBoard, updatedAt\) |
| `openings` | json | List of openings \(id, openedAt, closedAt, isArchived, archivedAt, closeReasonId, openingState, latestVersion with identifier/description/authorId/createdAt/teamId/jobIds\[\]/targetHireDate/targetStartDate/isBackfill/employmentType/locationIds\[\]/hiringTeam\[\]/customFields\[\]\) |
| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt, managerId\) |
| `interviewSchedules` | json | List of interview schedules \(id, applicationId, interviewStageId, interviewEvents\[\] with interviewerUserIds/startTime/endTime/feedbackLink/location/meetingLink/hasSubmittedFeedback, status, scheduledBy, createdAt, updatedAt\) |
| `tags` | json | List of candidate tags \(id, title, isArchived\) |
| `id` | string | Resource UUID |
| `name` | string | Resource name |
| `title` | string | Job title or job posting title |
| `status` | string | Status |
| `candidate` | json | Candidate details \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], customFields\[\], source, creditedToUser, createdAt, updatedAt\) |
| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], compensation, createdAt, updatedAt\) |
| `application` | json | Application details \(id, status, customFields\[\], candidate, currentInterviewStage, source, archiveReason, job, hiringTeam\[\], createdAt, updatedAt\) |
| `offer` | json | Offer details \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion\) |
| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, linkedData, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt\) |
| `content` | string | Note content |
| `author` | json | Note author \(id, firstName, lastName, email, globalRole, isEnabled\) |
| `isPrivate` | boolean | Whether the note is private |
| `createdAt` | string | ISO 8601 creation timestamp |
| `moreDataAvailable` | boolean | Whether more pages exist |
| `nextCursor` | string | Pagination cursor for next page |
| `syncToken` | string | Sync token for incremental updates |
### `ashby_list_applications`
Lists all applications in an Ashby organization with pagination and optional filters for status, job, candidate, and creation date.
Lists all applications in an Ashby organization with pagination and optional filters for status, job, and creation date.
#### Input
@@ -315,7 +537,6 @@ Lists all applications in an Ashby organization with pagination and optional fil
| `perPage` | number | No | Number of results per page \(default 100\) |
| `status` | string | No | Filter by application status: Active, Hired, Archived, or Lead |
| `jobId` | string | No | Filter applications by a specific job UUID |
| `candidateId` | string | No | Filter applications by a specific candidate UUID |
| `createdAfter` | string | No | Filter to applications created after this ISO 8601 timestamp \(e.g. 2024-01-01T00:00:00Z\) |
#### Output
@@ -323,23 +544,6 @@ Lists all applications in an Ashby organization with pagination and optional fil
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `applications` | array | List of applications |
| ↳ `id` | string | Application UUID |
| ↳ `status` | string | Application status \(Active, Hired, Archived, Lead\) |
| ↳ `candidate` | object | Associated candidate |
| ↳ `id` | string | Candidate UUID |
| ↳ `name` | string | Candidate name |
| ↳ `job` | object | Associated job |
| ↳ `id` | string | Job UUID |
| ↳ `title` | string | Job title |
| ↳ `currentInterviewStage` | object | Current interview stage |
| ↳ `id` | string | Stage UUID |
| ↳ `title` | string | Stage title |
| ↳ `type` | string | Stage type |
| ↳ `source` | object | Application source |
| ↳ `id` | string | Source UUID |
| ↳ `title` | string | Source title |
| ↳ `createdAt` | string | ISO 8601 creation timestamp |
| ↳ `updatedAt` | string | ISO 8601 last update timestamp |
| `moreDataAvailable` | boolean | Whether more pages of results exist |
| `nextCursor` | string | Opaque cursor for fetching the next page |
@@ -352,6 +556,7 @@ Lists all archive reasons configured in Ashby.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Ashby API Key |
| `includeArchived` | boolean | No | Whether to include archived archive reasons in the response \(default false\) |
#### Output
@@ -360,7 +565,7 @@ Lists all archive reasons configured in Ashby.
| `archiveReasons` | array | List of archive reasons |
| ↳ `id` | string | Archive reason UUID |
| ↳ `text` | string | Archive reason text |
| ↳ `reasonType` | string | Reason type |
| ↳ `reasonType` | string | Reason type \(RejectedByCandidate, RejectedByOrg, Other\) |
| ↳ `isArchived` | boolean | Whether the reason is archived |
### `ashby_list_candidate_tags`
@@ -372,6 +577,10 @@ Lists all candidate tags configured in Ashby.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Ashby API Key |
| `includeArchived` | boolean | No | Whether to include archived candidate tags \(default false\) |
| `cursor` | string | No | Opaque pagination cursor from a previous response nextCursor value |
| `syncToken` | string | No | Sync token from a previous response to fetch only changed results |
| `perPage` | number | No | Number of results per page \(default 100\) |
#### Output
@@ -381,6 +590,9 @@ Lists all candidate tags configured in Ashby.
| ↳ `id` | string | Tag UUID |
| ↳ `title` | string | Tag title |
| ↳ `isArchived` | boolean | Whether the tag is archived |
| `moreDataAvailable` | boolean | Whether more pages of results exist |
| `nextCursor` | string | Opaque cursor for fetching the next page |
| `syncToken` | string | Sync token to use for incremental updates in future requests |
### `ashby_list_candidates`
@@ -399,18 +611,6 @@ Lists all candidates in an Ashby organization with cursor-based pagination.
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `candidates` | array | List of candidates |
| ↳ `id` | string | Candidate UUID |
| ↳ `name` | string | Full name |
| ↳ `primaryEmailAddress` | object | Primary email contact info |
| ↳ `value` | string | Email address |
| ↳ `type` | string | Contact type \(Personal, Work, Other\) |
| ↳ `isPrimary` | boolean | Whether this is the primary email |
| ↳ `primaryPhoneNumber` | object | Primary phone contact info |
| ↳ `value` | string | Phone number |
| ↳ `type` | string | Contact type \(Personal, Work, Other\) |
| ↳ `isPrimary` | boolean | Whether this is the primary phone |
| ↳ `createdAt` | string | ISO 8601 creation timestamp |
| ↳ `updatedAt` | string | ISO 8601 last update timestamp |
| `moreDataAvailable` | boolean | Whether more pages of results exist |
| `nextCursor` | string | Opaque cursor for fetching the next page |
@@ -431,9 +631,15 @@ Lists all custom field definitions configured in Ashby.
| `customFields` | array | List of custom field definitions |
| ↳ `id` | string | Custom field UUID |
| ↳ `title` | string | Custom field title |
| ↳ `fieldType` | string | Field type \(e.g. String, Number, Boolean\) |
| ↳ `objectType` | string | Object type the field applies to \(e.g. Candidate, Application, Job\) |
| ↳ `isPrivate` | boolean | Whether the custom field is private |
| ↳ `fieldType` | string | Field data type \(MultiValueSelect, NumberRange, String, Date, ValueSelect, Number, Currency, Boolean, LongText, CompensationRange\) |
| ↳ `objectType` | string | Object type the field applies to \(Application, Candidate, Employee, Job, Offer, Opening, Talent_Project\) |
| ↳ `isArchived` | boolean | Whether the custom field is archived |
| ↳ `isRequired` | boolean | Whether a value is required |
| ↳ `selectableValues` | array | Selectable values for MultiValueSelect fields \(empty for other field types\) |
| ↳ `label` | string | Display label |
| ↳ `value` | string | Stored value |
| ↳ `isArchived` | boolean | Whether archived |
### `ashby_list_departments`
@@ -452,8 +658,11 @@ Lists all departments in Ashby.
| `departments` | array | List of departments |
| ↳ `id` | string | Department UUID |
| ↳ `name` | string | Department name |
| ↳ `externalName` | string | Candidate-facing name used on job boards |
| ↳ `isArchived` | boolean | Whether the department is archived |
| ↳ `parentId` | string | Parent department UUID |
| ↳ `createdAt` | string | ISO 8601 creation timestamp |
| ↳ `updatedAt` | string | ISO 8601 last update timestamp |
### `ashby_list_interviews`
@@ -475,10 +684,24 @@ Lists interview schedules in Ashby, optionally filtered by application or interv
| --------- | ---- | ----------- |
| `interviewSchedules` | array | List of interview schedules |
| ↳ `id` | string | Interview schedule UUID |
| ↳ `status` | string | Schedule status \(NeedsScheduling, WaitingOnCandidateBooking, Scheduled, Complete, Cancelled, OnHold, etc.\) |
| ↳ `applicationId` | string | Associated application UUID |
| ↳ `interviewStageId` | string | Interview stage UUID |
| ↳ `status` | string | Schedule status |
| ↳ `createdAt` | string | ISO 8601 creation timestamp |
| ↳ `updatedAt` | string | ISO 8601 last update timestamp |
| ↳ `interviewEvents` | array | Scheduled interview events on this schedule |
| ↳ `id` | string | Event UUID |
| ↳ `interviewId` | string | Interview template UUID |
| ↳ `interviewScheduleId` | string | Parent schedule UUID |
| ↳ `interviewerUserIds` | array | User UUIDs of interviewers assigned to the event |
| ↳ `createdAt` | string | Event creation timestamp |
| ↳ `updatedAt` | string | Event last updated timestamp |
| ↳ `startTime` | string | Event start time |
| ↳ `endTime` | string | Event end time |
| ↳ `feedbackLink` | string | URL to submit feedback for the event |
| ↳ `location` | string | Physical location |
| ↳ `meetingLink` | string | Virtual meeting URL |
| ↳ `hasSubmittedFeedback` | boolean | Whether any feedback has been submitted |
| `moreDataAvailable` | boolean | Whether more pages of results exist |
| `nextCursor` | string | Opaque cursor for fetching the next page |
@@ -500,11 +723,22 @@ Lists all job postings in Ashby.
| ↳ `id` | string | Job posting UUID |
| ↳ `title` | string | Job posting title |
| ↳ `jobId` | string | Associated job UUID |
| ↳ `locationName` | string | Location name |
| ↳ `departmentName` | string | Department name |
| ↳ `employmentType` | string | Employment type \(e.g. FullTime, PartTime, Contract\) |
| ↳ `teamName` | string | Team name |
| ↳ `locationName` | string | Primary location display name |
| ↳ `locationIds` | object | Primary and secondary location UUIDs |
| ↳ `primaryLocationId` | string | Primary location UUID |
| ↳ `secondaryLocationIds` | array | Secondary location UUIDs |
| ↳ `workplaceType` | string | Workplace type \(OnSite, Remote, Hybrid\) |
| ↳ `employmentType` | string | Employment type \(FullTime, PartTime, Intern, Contract, Temporary\) |
| ↳ `isListed` | boolean | Whether the posting is publicly listed |
| ↳ `publishedDate` | string | ISO 8601 published date |
| ↳ `applicationDeadline` | string | ISO 8601 application deadline |
| ↳ `externalLink` | string | External link to the job posting |
| ↳ `applyLink` | string | Direct apply link for the job posting |
| ↳ `compensationTierSummary` | string | Compensation tier summary for job boards |
| ↳ `shouldDisplayCompensationOnJobBoard` | boolean | Whether compensation is shown on the job board |
| ↳ `updatedAt` | string | ISO 8601 last update timestamp |
### `ashby_list_jobs`
@@ -524,14 +758,6 @@ Lists all jobs in an Ashby organization. By default returns Open, Closed, and Ar
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `jobs` | array | List of jobs |
| ↳ `id` | string | Job UUID |
| ↳ `title` | string | Job title |
| ↳ `status` | string | Job status \(Open, Closed, Archived, Draft\) |
| ↳ `employmentType` | string | Employment type \(FullTime, PartTime, Intern, Contract, Temporary\) |
| ↳ `departmentId` | string | Department UUID |
| ↳ `locationId` | string | Location UUID |
| ↳ `createdAt` | string | ISO 8601 creation timestamp |
| ↳ `updatedAt` | string | ISO 8601 last update timestamp |
| `moreDataAvailable` | boolean | Whether more pages of results exist |
| `nextCursor` | string | Opaque cursor for fetching the next page |
@@ -552,12 +778,18 @@ Lists all locations configured in Ashby.
| `locations` | array | List of locations |
| ↳ `id` | string | Location UUID |
| ↳ `name` | string | Location name |
| ↳ `externalName` | string | Candidate-facing name used on job boards |
| ↳ `isArchived` | boolean | Whether the location is archived |
| ↳ `isRemote` | boolean | Whether this is a remote location |
| ↳ `address` | object | Location address |
| ↳ `city` | string | City |
| ↳ `region` | string | State or region |
| ↳ `country` | string | Country |
| ↳ `isRemote` | boolean | Whether the location is remote \(use workplaceType instead\) |
| ↳ `workplaceType` | string | Workplace type \(OnSite, Hybrid, Remote\) |
| ↳ `parentLocationId` | string | Parent location UUID |
| ↳ `type` | string | Location component type \(Location, LocationHierarchy\) |
| ↳ `address` | object | Location postal address |
| ↳ `addressCountry` | string | Country |
| ↳ `addressRegion` | string | State or region |
| ↳ `addressLocality` | string | City or locality |
| ↳ `postalCode` | string | Postal code |
| ↳ `streetAddress` | string | Street address |
### `ashby_list_notes`
@@ -579,6 +811,7 @@ Lists all notes on a candidate with pagination support.
| `notes` | array | List of notes on the candidate |
| ↳ `id` | string | Note UUID |
| ↳ `content` | string | Note content |
| ↳ `isPrivate` | boolean | Whether the note is private |
| ↳ `author` | object | Note author |
| ↳ `id` | string | Author user UUID |
| ↳ `firstName` | string | First name |
@@ -605,16 +838,6 @@ Lists all offers with their latest version in an Ashby organization.
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `offers` | array | List of offers |
| ↳ `id` | string | Offer UUID |
| ↳ `offerStatus` | string | Offer status |
| ↳ `acceptanceStatus` | string | Acceptance status |
| ↳ `applicationId` | string | Associated application UUID |
| ↳ `startDate` | string | Offer start date |
| ↳ `salary` | object | Salary details |
| ↳ `currencyCode` | string | ISO 4217 currency code |
| ↳ `value` | number | Salary amount |
| ↳ `openingId` | string | Associated opening UUID |
| ↳ `createdAt` | string | ISO 8601 creation timestamp |
| `moreDataAvailable` | boolean | Whether more pages of results exist |
| `nextCursor` | string | Opaque cursor for fetching the next page |
@@ -634,12 +857,6 @@ Lists all openings in Ashby with pagination.
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `openings` | array | List of openings |
| ↳ `id` | string | Opening UUID |
| ↳ `openingState` | string | Opening state \(Approved, Closed, Draft, Filled, Open\) |
| ↳ `isArchived` | boolean | Whether the opening is archived |
| ↳ `openedAt` | string | ISO 8601 opened timestamp |
| ↳ `closedAt` | string | ISO 8601 closed timestamp |
| `moreDataAvailable` | boolean | Whether more pages of results exist |
| `nextCursor` | string | Opaque cursor for fetching the next page |
@@ -661,6 +878,10 @@ Lists all candidate sources configured in Ashby.
| ↳ `id` | string | Source UUID |
| ↳ `title` | string | Source title |
| ↳ `isArchived` | boolean | Whether the source is archived |
| ↳ `sourceType` | object | Source type grouping |
| ↳ `id` | string | Source type UUID |
| ↳ `title` | string | Source type title |
| ↳ `isArchived` | boolean | Whether archived |
### `ashby_list_users`
@@ -679,18 +900,12 @@ Lists all users in Ashby with pagination.
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `users` | array | List of users |
| ↳ `id` | string | User UUID |
| ↳ `firstName` | string | First name |
| ↳ `lastName` | string | Last name |
| ↳ `email` | string | Email address |
| ↳ `isEnabled` | boolean | Whether the user account is enabled |
| ↳ `globalRole` | string | User role \(Organization Admin, Elevated Access, Limited Access, External Recruiter\) |
| `moreDataAvailable` | boolean | Whether more pages of results exist |
| `nextCursor` | string | Opaque cursor for fetching the next page |
### `ashby_remove_candidate_tag`
Removes a tag from a candidate in Ashby.
Removes a tag from a candidate in Ashby and returns the updated candidate.
#### Input
@@ -704,7 +919,37 @@ Removes a tag from a candidate in Ashby.
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the tag was successfully removed |
| `candidates` | json | List of candidates with rich fields \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], linkedInUrl, githubUrl, profileUrl, position, company, school, timezone, location with locationComponents\[\], tags\[\], applicationIds\[\], customFields\[\], resumeFileHandle, fileHandles\[\], source with sourceType, creditedToUser, fraudStatus, createdAt, updatedAt\) |
| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion, compensation with compensationTiers\[\]\) |
| `applications` | json | List of applications \(id, status, customFields\[\], candidate summary, currentInterviewStage, source with sourceType, archiveReason with customFields\[\], archivedAt, job summary, creditedToUser, hiringTeam\[\], appliedViaJobPostingId, submitterClientIp, submitterUserAgent, createdAt, updatedAt\) |
| `notes` | json | List of notes \(id, content, author, isPrivate, createdAt\) |
| `offers` | json | List of offers \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion with id/startDate/salary/createdAt/openingId/customFields\[\]/fileHandles\[\]/author/approvalStatus\) |
| `archiveReasons` | json | List of archive reasons \(id, text, reasonType \[RejectedByCandidate/RejectedByOrg/Other\], isArchived\) |
| `sources` | json | List of sources \(id, title, isArchived, sourceType \{id, title, isArchived\}\) |
| `customFields` | json | List of custom field definitions \(id, title, isPrivate, fieldType, objectType, isArchived, isRequired, selectableValues\[\] \{label, value, isArchived\}\) |
| `departments` | json | List of departments \(id, name, externalName, isArchived, parentId, createdAt, updatedAt\) |
| `locations` | json | List of locations \(id, name, externalName, isArchived, isRemote, workplaceType, parentLocationId, type, address with addressCountry/Region/Locality/postalCode/streetAddress\) |
| `jobPostings` | json | List of job postings \(id, title, jobId, departmentName, teamName, locationName, locationIds, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensationTierSummary, shouldDisplayCompensationOnJobBoard, updatedAt\) |
| `openings` | json | List of openings \(id, openedAt, closedAt, isArchived, archivedAt, closeReasonId, openingState, latestVersion with identifier/description/authorId/createdAt/teamId/jobIds\[\]/targetHireDate/targetStartDate/isBackfill/employmentType/locationIds\[\]/hiringTeam\[\]/customFields\[\]\) |
| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt, managerId\) |
| `interviewSchedules` | json | List of interview schedules \(id, applicationId, interviewStageId, interviewEvents\[\] with interviewerUserIds/startTime/endTime/feedbackLink/location/meetingLink/hasSubmittedFeedback, status, scheduledBy, createdAt, updatedAt\) |
| `tags` | json | List of candidate tags \(id, title, isArchived\) |
| `id` | string | Resource UUID |
| `name` | string | Resource name |
| `title` | string | Job title or job posting title |
| `status` | string | Status |
| `candidate` | json | Candidate details \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], customFields\[\], source, creditedToUser, createdAt, updatedAt\) |
| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], compensation, createdAt, updatedAt\) |
| `application` | json | Application details \(id, status, customFields\[\], candidate, currentInterviewStage, source, archiveReason, job, hiringTeam\[\], createdAt, updatedAt\) |
| `offer` | json | Offer details \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion\) |
| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, linkedData, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt\) |
| `content` | string | Note content |
| `author` | json | Note author \(id, firstName, lastName, email, globalRole, isEnabled\) |
| `isPrivate` | boolean | Whether the note is private |
| `createdAt` | string | ISO 8601 creation timestamp |
| `moreDataAvailable` | boolean | Whether more pages exist |
| `nextCursor` | string | Pagination cursor for next page |
| `syncToken` | string | Sync token for incremental updates |
### `ashby_search_candidates`
@@ -723,18 +968,6 @@ Searches for candidates by name and/or email with AND logic. Results are limited
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `candidates` | array | Matching candidates \(max 100 results\) |
| ↳ `id` | string | Candidate UUID |
| ↳ `name` | string | Full name |
| ↳ `primaryEmailAddress` | object | Primary email contact info |
| ↳ `value` | string | Email address |
| ↳ `type` | string | Contact type \(Personal, Work, Other\) |
| ↳ `isPrimary` | boolean | Whether this is the primary email |
| ↳ `primaryPhoneNumber` | object | Primary phone contact info |
| ↳ `value` | string | Phone number |
| ↳ `type` | string | Contact type \(Personal, Work, Other\) |
| ↳ `isPrimary` | boolean | Whether this is the primary phone |
| ↳ `createdAt` | string | ISO 8601 creation timestamp |
| ↳ `updatedAt` | string | ISO 8601 last update timestamp |
### `ashby_update_candidate`
@@ -758,26 +991,36 @@ Updates an existing candidate record in Ashby. Only provided fields are changed.
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Candidate UUID |
| `name` | string | Full name |
| `primaryEmailAddress` | object | Primary email contact info |
| ↳ `value` | string | Email address |
| ↳ `type` | string | Contact type \(Personal, Work, Other\) |
| ↳ `isPrimary` | boolean | Whether this is the primary email |
| `primaryPhoneNumber` | object | Primary phone contact info |
| ↳ `value` | string | Phone number |
| ↳ `type` | string | Contact type \(Personal, Work, Other\) |
| ↳ `isPrimary` | boolean | Whether this is the primary phone |
| `profileUrl` | string | URL to the candidate Ashby profile |
| `position` | string | Current position or title |
| `company` | string | Current company |
| `linkedInUrl` | string | LinkedIn profile URL |
| `githubUrl` | string | GitHub profile URL |
| `tags` | array | Tags applied to the candidate |
| ↳ `id` | string | Tag UUID |
| `title` | string | Tag title |
| `applicationIds` | array | IDs of associated applications |
| `candidates` | json | List of candidates with rich fields \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], linkedInUrl, githubUrl, profileUrl, position, company, school, timezone, location with locationComponents\[\], tags\[\], applicationIds\[\], customFields\[\], resumeFileHandle, fileHandles\[\], source with sourceType, creditedToUser, fraudStatus, createdAt, updatedAt\) |
| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion, compensation with compensationTiers\[\]\) |
| `applications` | json | List of applications \(id, status, customFields\[\], candidate summary, currentInterviewStage, source with sourceType, archiveReason with customFields\[\], archivedAt, job summary, creditedToUser, hiringTeam\[\], appliedViaJobPostingId, submitterClientIp, submitterUserAgent, createdAt, updatedAt\) |
| `notes` | json | List of notes \(id, content, author, isPrivate, createdAt\) |
| `offers` | json | List of offers \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion with id/startDate/salary/createdAt/openingId/customFields\[\]/fileHandles\[\]/author/approvalStatus\) |
| `archiveReasons` | json | List of archive reasons \(id, text, reasonType \[RejectedByCandidate/RejectedByOrg/Other\], isArchived\) |
| `sources` | json | List of sources \(id, title, isArchived, sourceType \{id, title, isArchived\}\) |
| `customFields` | json | List of custom field definitions \(id, title, isPrivate, fieldType, objectType, isArchived, isRequired, selectableValues\[\] \{label, value, isArchived\}\) |
| `departments` | json | List of departments \(id, name, externalName, isArchived, parentId, createdAt, updatedAt\) |
| `locations` | json | List of locations \(id, name, externalName, isArchived, isRemote, workplaceType, parentLocationId, type, address with addressCountry/Region/Locality/postalCode/streetAddress\) |
| `jobPostings` | json | List of job postings \(id, title, jobId, departmentName, teamName, locationName, locationIds, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensationTierSummary, shouldDisplayCompensationOnJobBoard, updatedAt\) |
| `openings` | json | List of openings \(id, openedAt, closedAt, isArchived, archivedAt, closeReasonId, openingState, latestVersion with identifier/description/authorId/createdAt/teamId/jobIds\[\]/targetHireDate/targetStartDate/isBackfill/employmentType/locationIds\[\]/hiringTeam\[\]/customFields\[\]\) |
| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt, managerId\) |
| `interviewSchedules` | json | List of interview schedules \(id, applicationId, interviewStageId, interviewEvents\[\] with interviewerUserIds/startTime/endTime/feedbackLink/location/meetingLink/hasSubmittedFeedback, status, scheduledBy, createdAt, updatedAt\) |
| `tags` | json | List of candidate tags \(id, title, isArchived\) |
| `id` | string | Resource UUID |
| `name` | string | Resource name |
| `title` | string | Job title or job posting title |
| `status` | string | Status |
| `candidate` | json | Candidate details \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], customFields\[\], source, creditedToUser, createdAt, updatedAt\) |
| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], compensation, createdAt, updatedAt\) |
| `application` | json | Application details \(id, status, customFields\[\], candidate, currentInterviewStage, source, archiveReason, job, hiringTeam\[\], createdAt, updatedAt\) |
| `offer` | json | Offer details \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion\) |
| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, linkedData, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt\) |
| `content` | string | Note content |
| `author` | json | Note author \(id, firstName, lastName, email, globalRole, isEnabled\) |
| `isPrivate` | boolean | Whether the note is private |
| `createdAt` | string | ISO 8601 creation timestamp |
| `updatedAt` | string | ISO 8601 last update timestamp |
| `moreDataAvailable` | boolean | Whether more pages exist |
| `nextCursor` | string | Pagination cursor for next page |
| `syncToken` | string | Sync token for incremental updates |

View File

@@ -42,9 +42,18 @@ Runs a browser automation task using BrowserUse
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `task` | string | Yes | What should the browser agent do |
| `variables` | json | No | Optional variables to use as secrets \(format: \{key: value\}\) |
| `save_browser_data` | boolean | No | Whether to save browser data |
| `model` | string | No | LLM model to use \(default: gpt-4o\) |
| `startUrl` | string | No | Initial page URL to start the agent on \(reduces navigation steps\) |
| `variables` | json | No | Optional secrets injected into the task \(format: \{key: value\}\) |
| `allowedDomains` | string | No | Comma-separated list of domains the agent is allowed to visit |
| `maxSteps` | number | No | Maximum number of steps the agent may take \(default 100, max 10000\) |
| `flashMode` | boolean | No | Enable flash mode \(faster, less careful navigation\) |
| `thinking` | boolean | No | Enable extended reasoning mode |
| `vision` | string | No | Vision capability: "true", "false", or "auto" |
| `systemPromptExtension` | string | No | Optional text appended to the agent system prompt \(max 2000 chars\) |
| `structuredOutput` | string | No | Stringified JSON schema for the structured output |
| `highlightElements` | boolean | No | Highlight interactive elements on the page \(default true\) |
| `metadata` | json | No | Custom key-value metadata \(up to 10 pairs\) for tracking |
| `model` | string | No | LLM model identifier \(e.g. browser-use-2.0\) |
| `apiKey` | string | Yes | API key for BrowserUse API |
| `profile_id` | string | No | Browser profile ID for persistent sessions \(cookies, login state\) |
@@ -54,7 +63,18 @@ Runs a browser automation task using BrowserUse
| --------- | ---- | ----------- |
| `id` | string | Task execution identifier |
| `success` | boolean | Task completion status |
| `output` | json | Task output data |
| `steps` | json | Execution steps taken |
| `output` | json | Final task output \(string or structured\) |
| `steps` | array | Steps the agent executed \(number, memory, nextGoal, url, actions, duration\) |
| ↳ `number` | number | Sequential step number |
| ↳ `memory` | string | Agent memory at this step |
| ↳ `evaluationPreviousGoal` | string | Evaluation of previous goal completion |
| ↳ `nextGoal` | string | Goal for the next step |
| ↳ `url` | string | Current URL of the browser |
| ↳ `screenshotUrl` | string | Optional screenshot URL |
| ↳ `actions` | array | Stringified JSON actions performed |
| ↳ `duration` | number | Step duration in seconds |
| `liveUrl` | string | Embeddable live browser session URL \(active during execution\) |
| `shareUrl` | string | Public shareable URL for the recorded session \(post-run\) |
| `sessionId` | string | Browser Use session identifier |

View File

@@ -3,6 +3,7 @@
"index",
"a2a",
"agentmail",
"agentphone",
"agiloft",
"ahrefs",
"airtable",
@@ -149,6 +150,7 @@
"rootly",
"s3",
"salesforce",
"sap_s4hana",
"search",
"secrets_manager",
"sendgrid",

File diff suppressed because it is too large Load Diff

View File

@@ -925,6 +925,139 @@ Create a canvas pinned to a Slack channel as its resource hub
| --------- | ---- | ----------- |
| `canvas_id` | string | ID of the created channel canvas |
### `slack_get_canvas`
Get Slack canvas file metadata by canvas ID
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `authMethod` | string | No | Authentication method: oauth or bot_token |
| `botToken` | string | No | Bot token for Custom Bot |
| `canvasId` | string | Yes | Canvas file ID to retrieve \(e.g., F1234ABCD\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `canvas` | object | Canvas file information returned by Slack |
| ↳ `id` | string | Unique canvas file identifier |
| ↳ `created` | number | Unix timestamp when the canvas was created |
| ↳ `timestamp` | number | Unix timestamp associated with the canvas |
| ↳ `name` | string | Canvas file name |
| ↳ `title` | string | Canvas title |
| ↳ `mimetype` | string | MIME type of the canvas file |
| ↳ `filetype` | string | Slack file type for the canvas |
| ↳ `pretty_type` | string | Human-readable file type |
| ↳ `user` | string | User ID of the canvas creator |
| ↳ `editable` | boolean | Whether the canvas file is editable |
| ↳ `size` | number | Canvas file size in bytes |
| ↳ `mode` | string | File mode |
| ↳ `is_external` | boolean | Whether the canvas is externally hosted |
| ↳ `is_public` | boolean | Whether the canvas is public |
| ↳ `url_private` | string | Private URL for the canvas file |
| ↳ `url_private_download` | string | Private download URL for the canvas file |
| ↳ `permalink` | string | Permanent URL for the canvas |
| ↳ `channels` | array | Public channel IDs where the canvas appears |
| ↳ `groups` | array | Private channel IDs where the canvas appears |
| ↳ `ims` | array | Direct message IDs where the canvas appears |
| ↳ `canvas_readtime` | number | Approximate read time for canvas content |
| ↳ `is_channel_space` | boolean | Whether this canvas is linked to a channel |
| ↳ `linked_channel_id` | string | Channel ID linked to this canvas |
| ↳ `canvas_creator_id` | string | User ID of the canvas creator |
### `slack_list_canvases`
List Slack canvases available to the authenticated user or bot
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `authMethod` | string | No | Authentication method: oauth or bot_token |
| `botToken` | string | No | Bot token for Custom Bot |
| `channel` | string | No | Filter canvases appearing in a specific channel ID |
| `count` | number | No | Number of canvases to return per page |
| `page` | number | No | Page number to return |
| `user` | string | No | Filter canvases created by a single user ID |
| `tsFrom` | string | No | Filter canvases created after this Unix timestamp |
| `tsTo` | string | No | Filter canvases created before this Unix timestamp |
| `teamId` | string | No | Encoded team ID, required when using an org-level token |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `canvases` | array | Canvas file objects returned by Slack |
| ↳ `id` | string | Unique canvas file identifier |
| ↳ `created` | number | Unix timestamp when the canvas was created |
| ↳ `timestamp` | number | Unix timestamp associated with the canvas |
| ↳ `name` | string | Canvas file name |
| ↳ `title` | string | Canvas title |
| ↳ `mimetype` | string | MIME type of the canvas file |
| ↳ `filetype` | string | Slack file type for the canvas |
| ↳ `pretty_type` | string | Human-readable file type |
| ↳ `user` | string | User ID of the canvas creator |
| ↳ `editable` | boolean | Whether the canvas file is editable |
| ↳ `size` | number | Canvas file size in bytes |
| ↳ `mode` | string | File mode |
| ↳ `is_external` | boolean | Whether the canvas is externally hosted |
| ↳ `is_public` | boolean | Whether the canvas is public |
| ↳ `url_private` | string | Private URL for the canvas file |
| ↳ `url_private_download` | string | Private download URL for the canvas file |
| ↳ `permalink` | string | Permanent URL for the canvas |
| ↳ `channels` | array | Public channel IDs where the canvas appears |
| ↳ `groups` | array | Private channel IDs where the canvas appears |
| ↳ `ims` | array | Direct message IDs where the canvas appears |
| ↳ `canvas_readtime` | number | Approximate read time for canvas content |
| ↳ `is_channel_space` | boolean | Whether this canvas is linked to a channel |
| ↳ `linked_channel_id` | string | Channel ID linked to this canvas |
| ↳ `canvas_creator_id` | string | User ID of the canvas creator |
| `paging` | object | Pagination information from Slack |
| ↳ `count` | number | Number of items requested per page |
| ↳ `total` | number | Total number of matching files |
| ↳ `page` | number | Current page number |
| ↳ `pages` | number | Total number of pages |
### `slack_lookup_canvas_sections`
Find Slack canvas section IDs matching criteria for later edits
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `authMethod` | string | No | Authentication method: oauth or bot_token |
| `botToken` | string | No | Bot token for Custom Bot |
| `canvasId` | string | Yes | Canvas ID to search \(e.g., F1234ABCD\) |
| `criteria` | json | Yes | Section lookup criteria, such as \{"section_types":\["h1"\],"contains_text":"Roadmap"\} |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `sections` | array | Canvas sections matching the lookup criteria |
| ↳ `id` | string | Canvas section identifier |
### `slack_delete_canvas`
Delete a Slack canvas by its canvas ID
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `authMethod` | string | No | Authentication method: oauth or bot_token |
| `botToken` | string | No | Bot token for Custom Bot |
| `canvasId` | string | Yes | Canvas ID to delete \(e.g., F1234ABCD\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ok` | boolean | Whether Slack deleted the canvas successfully |
### `slack_create_conversation`
Create a new public or private channel in a Slack workspace.

View File

@@ -72,6 +72,8 @@ Run an autonomous web agent to complete tasks and extract structured data
| `provider` | string | No | AI provider to use: openai or anthropic |
| `apiKey` | string | Yes | API key for the selected provider |
| `outputSchema` | json | No | Optional JSON schema defining the structure of data the agent should return |
| `mode` | string | No | Agent tool mode: dom \(default\), hybrid, or cua |
| `maxSteps` | number | No | Maximum agent steps \(default 20, max 200\) |
#### Output
@@ -92,5 +94,7 @@ Run an autonomous web agent to complete tasks and extract structured data
| ↳ `timestamp` | number | Unix timestamp when the action was performed |
| ↳ `timeMs` | number | Time in milliseconds \(for wait actions\) |
| `structuredOutput` | object | Extracted data matching the provided output schema |
| `liveViewUrl` | string | Embeddable Browserbase live view URL \(active only while the session is running\) |
| `sessionId` | string | Browserbase session identifier |

View File

@@ -97,6 +97,14 @@ Trigger workflow when a candidate is hired
| ↳ `job` | object | job output from the tool |
| ↳ `id` | string | Job UUID |
| ↳ `title` | string | Job title |
| `offer` | object | offer output from the tool |
| ↳ `id` | string | Accepted offer UUID |
| ↳ `applicationId` | string | Associated application UUID |
| ↳ `acceptanceStatus` | string | Offer acceptance status |
| ↳ `offerStatus` | string | Offer process status |
| ↳ `decidedAt` | string | Offer decision timestamp \(ISO 8601\) |
| ↳ `latestVersion` | object | latestVersion output from the tool |
| ↳ `id` | string | Latest offer version UUID |
---

View File

@@ -89,6 +89,8 @@ Polling Groups let you monitor multiple team members' Gmail or Outlook inboxes w
Invitees receive an email with a link to connect their account. Once connected, their inbox is automatically included in the polling group. Invitees don't need to be members of your Sim organization.
This is separate from external workspace membership: polling group invitees are granting access to an inbox for a trigger, while external workspace members are collaborators with Read, Write, or Admin access to a workspace.
**Using in a Workflow**
When configuring an email trigger, select your polling group from the credentials dropdown instead of an individual account. The system creates webhooks for each member and routes all emails through your workflow.

View File

@@ -49,7 +49,7 @@ Environment variables store sensitive values like API keys, tokens, and configur
| Scope | Visibility | Use case |
|-------|-----------|----------|
| **Workspace** | All workspace members | Shared API keys, team configuration |
| **Workspace** | All workspace members, including external workspace members | Shared API keys, team configuration |
| **Personal** | Only you | Your personal tokens, dev credentials |
When both a workspace and personal variable share the same key, the workspace value takes precedence.
@@ -84,7 +84,7 @@ If a workflow variable and a block output share the same name, Sim resolves the
<FAQ items={[
{ question: "What's the difference between workflow variables and environment variables?", answer: "Workflow variables store runtime data (text, numbers, objects, arrays) that blocks can read and modify during execution. They use <variable.name> syntax. Environment variables store sensitive configuration like API keys using {{KEY}} syntax. They never appear in logs and are managed at the workspace or personal level." },
{ question: "Can I use environment variables in the Function block?", answer: "Yes. Use the double curly brace syntax {{KEY}} directly in your code. The value is substituted before execution, so the actual secret never appears in logs or outputs." },
{ question: "How do I share an API key with my team?", answer: "Create a workspace-scoped environment variable in Settings → Secrets. All workspace members will be able to use it in their workflows via {{KEY}} syntax." },
{ question: "How do I share an API key with my team?", answer: "Create a workspace-scoped environment variable in Settings → Secrets. All workspace members, including external workspace members, will be able to use it in their workflows via {{KEY}} syntax." },
{ question: "What happens if a variable name has spaces or mixed case?", answer: "Variable resolution is case-insensitive and ignores spaces. A variable named 'My Counter' can be referenced as <variable.mycounter> or <variable.My Counter>. However, using consistent naming (like camelCase) is recommended." },
{ question: "Can I reference environment variables in the Agent system prompt?", answer: "Yes. You can use {{KEY}} syntax in any text field, including system prompts, to inject environment variable values." },
]} />

View File

@@ -0,0 +1,30 @@
# Environment variables required by the @sim/realtime (Socket.IO) server.
# These MUST match the corresponding values in apps/sim/.env for auth to work.
# See apps/realtime/src/env.ts for the full zod schema.
# Core
NODE_ENV=development
PORT=3002
# Database — must point at the same Postgres as the main app
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/simstudio
# Auth — shared with apps/sim (Better Auth "Shared Database Session" pattern)
BETTER_AUTH_URL=http://localhost:3000
BETTER_AUTH_SECRET=your_better_auth_secret_min_32_chars
# Internal RPC — shared with apps/sim
INTERNAL_API_SECRET=your_internal_api_secret_min_32_chars
# Public app URL — used for CORS allow-list and base URL resolution
NEXT_PUBLIC_APP_URL=http://localhost:3000
# Optional: Redis for cross-pod room management
# Leave unset for single-pod / in-memory rooms
# REDIS_URL=redis://localhost:6379
# Optional: extra Socket.IO CORS allow-list (comma-separated)
# ALLOWED_ORIGINS=https://embed.example.com,https://admin.example.com
# Optional: disable auth entirely for trusted private networks
# DISABLE_AUTH=true

View File

@@ -0,0 +1,48 @@
{
"name": "@sim/realtime",
"version": "0.1.0",
"private": true,
"license": "Apache-2.0",
"type": "module",
"engines": {
"bun": ">=1.2.13",
"node": ">=20.0.0"
},
"scripts": {
"dev": "bun --watch src/index.ts",
"start": "bun src/index.ts",
"type-check": "tsc --noEmit",
"lint": "biome check --write --unsafe .",
"lint:check": "biome check .",
"format": "biome format --write .",
"format:check": "biome format .",
"test": "vitest run",
"test:watch": "vitest"
},
"dependencies": {
"@sim/audit": "workspace:*",
"@sim/auth": "workspace:*",
"@sim/db": "workspace:*",
"@sim/logger": "workspace:*",
"@sim/realtime-protocol": "workspace:*",
"@sim/security": "workspace:*",
"@sim/utils": "workspace:*",
"@sim/workflow-authz": "workspace:*",
"@sim/workflow-persistence": "workspace:*",
"@sim/workflow-types": "workspace:*",
"@socket.io/redis-adapter": "8.3.0",
"drizzle-orm": "^0.45.2",
"postgres": "^3.4.5",
"redis": "5.10.0",
"socket.io": "^4.8.1",
"zod": "^3.24.2"
},
"devDependencies": {
"@sim/testing": "workspace:*",
"@sim/tsconfig": "workspace:*",
"@types/node": "24.2.1",
"socket.io-client": "4.8.1",
"typescript": "^5.7.3",
"vitest": "^3.0.8"
}
}

17
apps/realtime/src/auth.ts Normal file
View File

@@ -0,0 +1,17 @@
import { createVerifyAuth } from '@sim/auth/verify'
import { env } from '@/env'
export const ANONYMOUS_USER_ID = '00000000-0000-0000-0000-000000000000'
export const ANONYMOUS_USER = {
id: ANONYMOUS_USER_ID,
name: 'Anonymous',
email: 'anonymous@localhost',
emailVerified: true,
image: null,
} as const
export const auth = createVerifyAuth({
secret: env.BETTER_AUTH_SECRET,
baseURL: env.BETTER_AUTH_URL,
})

View File

@@ -3,9 +3,7 @@ import { createLogger } from '@sim/logger'
import { createAdapter } from '@socket.io/redis-adapter'
import { createClient, type RedisClientType } from 'redis'
import { Server } from 'socket.io'
import { env } from '@/lib/core/config/env'
import { isProd } from '@/lib/core/config/feature-flags'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { env, getBaseUrl, isProd } from '@/env'
const logger = createLogger('SocketIOConfig')

View File

@@ -1,15 +1,7 @@
import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
import * as schema from '@sim/db'
import { webhook, workflow, workflowBlocks, workflowEdges, workflowSubflows } from '@sim/db'
import { workflow, workflowBlocks, workflowEdges, workflowSubflows } from '@sim/db'
import { createLogger } from '@sim/logger'
import { and, eq, inArray, isNull, or, sql } from 'drizzle-orm'
import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { env } from '@/lib/core/config/env'
import { cleanupExternalWebhook } from '@/lib/webhooks/provider-subscriptions'
import { getActiveWorkflowContext } from '@/lib/workflows/active-context'
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
import { mergeSubBlockValues } from '@/lib/workflows/subblocks'
import {
BLOCK_OPERATIONS,
BLOCKS_OPERATIONS,
@@ -19,7 +11,14 @@ import {
SUBFLOW_OPERATIONS,
VARIABLE_OPERATIONS,
WORKFLOW_OPERATIONS,
} from '@/socket/constants'
} from '@sim/realtime-protocol/constants'
import { getActiveWorkflowContext } from '@sim/workflow-authz'
import { loadWorkflowFromNormalizedTablesRaw } from '@sim/workflow-persistence/load'
import { mergeSubBlockValues } from '@sim/workflow-persistence/subblocks'
import { and, eq, inArray, isNull, or, sql } from 'drizzle-orm'
import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
import { env } from '@/env'
const logger = createLogger('SocketDatabase')
@@ -182,7 +181,7 @@ export async function getWorkflowState(workflowId: string) {
throw new Error(`Workflow ${workflowId} not found`)
}
const normalizedData = await loadWorkflowFromNormalizedTables(workflowId)
const normalizedData = await loadWorkflowFromNormalizedTablesRaw(workflowId)
if (normalizedData) {
const finalState = {
@@ -915,30 +914,10 @@ async function handleBlocksOperationTx(
}
}
// Clean up external webhooks
const webhooksToCleanup = await tx
.select({
webhook: webhook,
workflow: {
id: workflow.id,
userId: workflow.userId,
workspaceId: workflow.workspaceId,
},
})
.from(webhook)
.innerJoin(workflow, eq(webhook.workflowId, workflow.id))
.where(and(eq(webhook.workflowId, workflowId), inArray(webhook.blockId, blockIdsArray)))
if (webhooksToCleanup.length > 0) {
const requestId = `socket-batch-${workflowId}-${Date.now()}`
for (const { webhook: wh, workflow: wf } of webhooksToCleanup) {
try {
await cleanupExternalWebhook(wh, wf, requestId)
} catch (error) {
logger.error(`Failed to cleanup webhook ${wh.id}:`, error)
}
}
}
// Webhook rows are only created at deploy time (saveTriggerWebhooksForDeploy in
// lib/webhooks/deploy.ts) with deploymentVersionId set; their external-subscription
// lifecycle is managed by deploy.ts, lifecycle.ts, and the /api/webhooks/[id] route.
// Removing a trigger block from the draft canvas does not touch any webhook rows.
// Delete edges connected to any of the blocks
await tx

44
apps/realtime/src/env.ts Normal file
View File

@@ -0,0 +1,44 @@
import { z } from 'zod'
const EnvSchema = z.object({
NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
DATABASE_URL: z.string().url(),
REDIS_URL: z.string().url().optional(),
BETTER_AUTH_URL: z.string().url(),
BETTER_AUTH_SECRET: z.string().min(32),
INTERNAL_API_SECRET: z.string().min(32),
NEXT_PUBLIC_APP_URL: z.string().url(),
ALLOWED_ORIGINS: z.string().optional(),
PORT: z.coerce.number().int().positive().default(3002),
DISABLE_AUTH: z
.string()
.optional()
.transform((value) => value === 'true' || value === '1'),
})
function parseEnv() {
const parsed = EnvSchema.safeParse(process.env)
if (!parsed.success) {
const formatted = parsed.error.format()
throw new Error(`Invalid realtime server environment: ${JSON.stringify(formatted, null, 2)}`)
}
return parsed.data
}
export const env = parseEnv()
export const isProd = env.NODE_ENV === 'production'
export const isDev = env.NODE_ENV === 'development'
export const isTest = env.NODE_ENV === 'test'
let appHostname = ''
try {
appHostname = new URL(env.NEXT_PUBLIC_APP_URL).hostname
} catch {}
export const isHosted = appHostname === 'sim.ai' || appHostname.endsWith('.sim.ai')
export const isAuthDisabled = env.DISABLE_AUTH === true && !isHosted
export function getBaseUrl(): string {
return env.NEXT_PUBLIC_APP_URL
}

View File

@@ -1,8 +1,8 @@
import { createLogger } from '@sim/logger'
import { cleanupPendingSubblocksForSocket } from '@/socket/handlers/subblocks'
import { cleanupPendingVariablesForSocket } from '@/socket/handlers/variables'
import type { AuthenticatedSocket } from '@/socket/middleware/auth'
import type { IRoomManager } from '@/socket/rooms'
import { cleanupPendingSubblocksForSocket } from '@/handlers/subblocks'
import { cleanupPendingVariablesForSocket } from '@/handlers/variables'
import type { AuthenticatedSocket } from '@/middleware/auth'
import type { IRoomManager } from '@/rooms'
const logger = createLogger('ConnectionHandlers')

View File

@@ -0,0 +1,17 @@
import { setupConnectionHandlers } from '@/handlers/connection'
import { setupOperationsHandlers } from '@/handlers/operations'
import { setupPresenceHandlers } from '@/handlers/presence'
import { setupSubblocksHandlers } from '@/handlers/subblocks'
import { setupVariablesHandlers } from '@/handlers/variables'
import { setupWorkflowHandlers } from '@/handlers/workflow'
import type { AuthenticatedSocket } from '@/middleware/auth'
import type { IRoomManager } from '@/rooms'
export function setupAllHandlers(socket: AuthenticatedSocket, roomManager: IRoomManager) {
setupWorkflowHandlers(socket, roomManager)
setupOperationsHandlers(socket, roomManager)
setupSubblocksHandlers(socket, roomManager)
setupVariablesHandlers(socket, roomManager)
setupPresenceHandlers(socket, roomManager)
setupConnectionHandlers(socket, roomManager)
}

View File

@@ -1,6 +1,4 @@
import { createLogger } from '@sim/logger'
import { generateId } from '@sim/utils/id'
import { ZodError } from 'zod'
import {
BLOCK_OPERATIONS,
BLOCKS_OPERATIONS,
@@ -9,12 +7,14 @@ import {
VARIABLE_OPERATIONS,
type VariableOperation,
WORKFLOW_OPERATIONS,
} from '@/socket/constants'
import { persistWorkflowOperation } from '@/socket/database/operations'
import type { AuthenticatedSocket } from '@/socket/middleware/auth'
import { checkRolePermission } from '@/socket/middleware/permissions'
import type { IRoomManager, UserSession } from '@/socket/rooms'
import { WorkflowOperationSchema } from '@/socket/validation/schemas'
} from '@sim/realtime-protocol/constants'
import { WorkflowOperationSchema } from '@sim/realtime-protocol/schemas'
import { generateId } from '@sim/utils/id'
import { ZodError } from 'zod'
import { persistWorkflowOperation } from '@/database/operations'
import type { AuthenticatedSocket } from '@/middleware/auth'
import { checkRolePermission } from '@/middleware/permissions'
import type { IRoomManager, UserSession } from '@/rooms'
const logger = createLogger('OperationsHandlers')

View File

@@ -1,6 +1,6 @@
import { createLogger } from '@sim/logger'
import type { AuthenticatedSocket } from '@/socket/middleware/auth'
import type { IRoomManager } from '@/socket/rooms'
import type { AuthenticatedSocket } from '@/middleware/auth'
import type { IRoomManager } from '@/rooms'
const logger = createLogger('PresenceHandlers')

View File

@@ -1,11 +1,11 @@
import { db } from '@sim/db'
import { workflow, workflowBlocks } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { SUBBLOCK_OPERATIONS } from '@sim/realtime-protocol/constants'
import { and, eq } from 'drizzle-orm'
import { SUBBLOCK_OPERATIONS } from '@/socket/constants'
import type { AuthenticatedSocket } from '@/socket/middleware/auth'
import { checkRolePermission } from '@/socket/middleware/permissions'
import type { IRoomManager } from '@/socket/rooms'
import type { AuthenticatedSocket } from '@/middleware/auth'
import { checkRolePermission } from '@/middleware/permissions'
import type { IRoomManager } from '@/rooms'
const logger = createLogger('SubblocksHandlers')

View File

@@ -1,11 +1,11 @@
import { db } from '@sim/db'
import { workflow } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { VARIABLE_OPERATIONS } from '@sim/realtime-protocol/constants'
import { eq } from 'drizzle-orm'
import { VARIABLE_OPERATIONS } from '@/socket/constants'
import type { AuthenticatedSocket } from '@/socket/middleware/auth'
import { checkRolePermission } from '@/socket/middleware/permissions'
import type { IRoomManager } from '@/socket/rooms'
import type { AuthenticatedSocket } from '@/middleware/auth'
import { checkRolePermission } from '@/middleware/permissions'
import type { IRoomManager } from '@/rooms'
const logger = createLogger('VariablesHandlers')

View File

@@ -2,7 +2,7 @@
* @vitest-environment node
*/
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { IRoomManager } from '@/socket/rooms'
import type { IRoomManager } from '@/rooms'
const { mockGetWorkflowState, mockVerifyWorkflowAccess } = vi.hoisted(() => ({
mockGetWorkflowState: vi.fn(),
@@ -14,15 +14,15 @@ vi.mock('@sim/db', () => ({
user: { image: 'image' },
}))
vi.mock('@/socket/database/operations', () => ({
vi.mock('@/database/operations', () => ({
getWorkflowState: mockGetWorkflowState,
}))
vi.mock('@/socket/middleware/permissions', () => ({
vi.mock('@/middleware/permissions', () => ({
verifyWorkflowAccess: mockVerifyWorkflowAccess,
}))
import { setupWorkflowHandlers } from '@/socket/handlers/workflow'
import { setupWorkflowHandlers } from '@/handlers/workflow'
interface JoinWorkflowPayload {
workflowId: string

View File

@@ -1,10 +1,10 @@
import { db, user } from '@sim/db'
import { createLogger } from '@sim/logger'
import { eq } from 'drizzle-orm'
import { getWorkflowState } from '@/socket/database/operations'
import type { AuthenticatedSocket } from '@/socket/middleware/auth'
import { verifyWorkflowAccess } from '@/socket/middleware/permissions'
import type { IRoomManager, UserPresence } from '@/socket/rooms'
import { getWorkflowState } from '@/database/operations'
import type { AuthenticatedSocket } from '@/middleware/auth'
import { verifyWorkflowAccess } from '@/middleware/permissions'
import type { IRoomManager, UserPresence } from '@/rooms'
const logger = createLogger('WorkflowHandlers')

View File

@@ -4,21 +4,28 @@
* @vitest-environment node
*/
import { createServer, request as httpRequest } from 'http'
import { createEnvMock, createMockLogger } from '@sim/testing'
import { createMockLogger } from '@sim/testing'
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
import { createSocketIOServer } from '@/socket/config/socket'
import { MemoryRoomManager } from '@/socket/rooms'
import { createHttpHandler } from '@/socket/routes/http'
import { createSocketIOServer } from '@/config/socket'
import { MemoryRoomManager } from '@/rooms'
import { createHttpHandler } from '@/routes/http'
vi.mock('@/lib/auth', () => ({
vi.mock('@/auth', () => ({
auth: {
api: {
verifyOneTimeToken: vi.fn(),
},
},
ANONYMOUS_USER_ID: '00000000-0000-0000-0000-000000000000',
ANONYMOUS_USER: {
id: '00000000-0000-0000-0000-000000000000',
name: 'Anonymous',
email: 'anonymous@localhost',
emailVerified: true,
image: null,
},
}))
// Mock redis package to prevent actual Redis connections
vi.mock('redis', () => ({
createClient: vi.fn(() => ({
on: vi.fn(),
@@ -28,15 +35,27 @@ vi.mock('redis', () => ({
})),
}))
vi.mock('@/lib/core/config/env', () =>
createEnvMock({
vi.mock('@/env', () => ({
env: {
DATABASE_URL: 'postgres://localhost/test',
NODE_ENV: 'test',
REDIS_URL: undefined,
})
)
BETTER_AUTH_URL: 'http://localhost:3000',
BETTER_AUTH_SECRET: 'test-better-auth-secret-at-least-32-chars',
INTERNAL_API_SECRET: 'test-internal-api-secret-at-least-32-chars',
NEXT_PUBLIC_APP_URL: 'http://localhost:3000',
PORT: 3002,
DISABLE_AUTH: false,
},
isProd: false,
isDev: false,
isTest: true,
isHosted: false,
isAuthDisabled: false,
getBaseUrl: () => 'http://localhost:3000',
}))
vi.mock('@/socket/middleware/auth', () => ({
vi.mock('@/middleware/auth', () => ({
authenticateSocket: vi.fn((socket, next) => {
socket.userId = 'test-user-id'
socket.userName = 'Test User'
@@ -45,7 +64,7 @@ vi.mock('@/socket/middleware/auth', () => ({
}),
}))
vi.mock('@/socket/middleware/permissions', () => ({
vi.mock('@/middleware/permissions', () => ({
verifyWorkflowAccess: vi.fn().mockResolvedValue({
hasAccess: true,
role: 'admin',
@@ -55,7 +74,7 @@ vi.mock('@/socket/middleware/permissions', () => ({
}),
}))
vi.mock('@/socket/database/operations', () => ({
vi.mock('@/database/operations', () => ({
getWorkflowState: vi.fn().mockResolvedValue({
id: 'test-workflow',
name: 'Test Workflow',
@@ -275,13 +294,13 @@ describe('Socket Server Index Integration', () => {
describe('Module Integration', () => {
it.concurrent('should properly import all extracted modules', async () => {
const { createSocketIOServer } = await import('@/socket/config/socket')
const { createHttpHandler } = await import('@/socket/routes/http')
const { MemoryRoomManager, RedisRoomManager } = await import('@/socket/rooms')
const { authenticateSocket } = await import('@/socket/middleware/auth')
const { verifyWorkflowAccess } = await import('@/socket/middleware/permissions')
const { getWorkflowState } = await import('@/socket/database/operations')
const { WorkflowOperationSchema } = await import('@/socket/validation/schemas')
const { createSocketIOServer } = await import('@/config/socket')
const { createHttpHandler } = await import('@/routes/http')
const { MemoryRoomManager, RedisRoomManager } = await import('@/rooms')
const { authenticateSocket } = await import('@/middleware/auth')
const { verifyWorkflowAccess } = await import('@/middleware/permissions')
const { getWorkflowState } = await import('@/database/operations')
const { WorkflowOperationSchema } = await import('@sim/realtime-protocol/schemas')
expect(createSocketIOServer).toBeTypeOf('function')
expect(createHttpHandler).toBeTypeOf('function')
@@ -332,7 +351,7 @@ describe('Socket Server Index Integration', () => {
describe('Validation and Utils', () => {
it.concurrent('should validate workflow operations', async () => {
const { WorkflowOperationSchema } = await import('@/socket/validation/schemas')
const { WorkflowOperationSchema } = await import('@sim/realtime-protocol/schemas')
const validOperation = {
operation: 'batch-add-blocks',
@@ -358,7 +377,7 @@ describe('Socket Server Index Integration', () => {
})
it.concurrent('should validate batch-add-blocks with edges', async () => {
const { WorkflowOperationSchema } = await import('@/socket/validation/schemas')
const { WorkflowOperationSchema } = await import('@sim/realtime-protocol/schemas')
const validOperationWithEdge = {
operation: 'batch-add-blocks',
@@ -393,7 +412,7 @@ describe('Socket Server Index Integration', () => {
})
it.concurrent('should validate edge operations', async () => {
const { WorkflowOperationSchema } = await import('@/socket/validation/schemas')
const { WorkflowOperationSchema } = await import('@sim/realtime-protocol/schemas')
const validEdgeOperation = {
operation: 'add',
@@ -410,7 +429,7 @@ describe('Socket Server Index Integration', () => {
})
it('should validate subflow operations', async () => {
const { WorkflowOperationSchema } = await import('@/socket/validation/schemas')
const { WorkflowOperationSchema } = await import('@sim/realtime-protocol/schemas')
const validSubflowOperation = {
operation: 'update',

View File

@@ -1,12 +1,12 @@
import { createServer } from 'http'
import { createLogger } from '@sim/logger'
import type { Server as SocketIOServer } from 'socket.io'
import { env } from '@/lib/core/config/env'
import { createSocketIOServer, shutdownSocketIOAdapter } from '@/socket/config/socket'
import { setupAllHandlers } from '@/socket/handlers'
import { type AuthenticatedSocket, authenticateSocket } from '@/socket/middleware/auth'
import { type IRoomManager, MemoryRoomManager, RedisRoomManager } from '@/socket/rooms'
import { createHttpHandler } from '@/socket/routes/http'
import { createSocketIOServer, shutdownSocketIOAdapter } from '@/config/socket'
import { env } from '@/env'
import { setupAllHandlers } from '@/handlers'
import { type AuthenticatedSocket, authenticateSocket } from '@/middleware/auth'
import { type IRoomManager, MemoryRoomManager, RedisRoomManager } from '@/rooms'
import { createHttpHandler } from '@/routes/http'
const logger = createLogger('CollaborativeSocketServer')
@@ -29,7 +29,7 @@ async function createRoomManager(io: SocketIOServer): Promise<IRoomManager> {
async function main() {
const httpServer = createServer()
const PORT = Number(env.PORT || env.SOCKET_PORT || 3002)
const PORT = env.PORT
logger.info('Starting Socket.IO server...', {
port: PORT,

View File

@@ -1,9 +1,8 @@
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import type { Socket } from 'socket.io'
import { auth } from '@/lib/auth'
import { ANONYMOUS_USER, ANONYMOUS_USER_ID } from '@/lib/auth/constants'
import { isAuthDisabled } from '@/lib/core/config/feature-flags'
import { ANONYMOUS_USER, ANONYMOUS_USER_ID, auth } from '@/auth'
import { isAuthDisabled } from '@/env'
const logger = createLogger('SocketAuth')

View File

@@ -14,7 +14,7 @@ import {
SOCKET_OPERATIONS,
} from '@sim/testing'
import { describe, expect, it } from 'vitest'
import { checkRolePermission } from '@/socket/middleware/permissions'
import { checkRolePermission } from '@/middleware/permissions'
describe('checkRolePermission', () => {
describe('admin role', () => {

View File

@@ -1,8 +1,6 @@
import { db } from '@sim/db'
import { workflow } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, eq, isNull } from 'drizzle-orm'
import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils'
import {
BLOCK_OPERATIONS,
BLOCKS_OPERATIONS,
@@ -12,7 +10,9 @@ import {
SUBFLOW_OPERATIONS,
VARIABLE_OPERATIONS,
WORKFLOW_OPERATIONS,
} from '@/socket/constants'
} from '@sim/realtime-protocol/constants'
import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz'
import { and, eq, isNull } from 'drizzle-orm'
const logger = createLogger('SocketPermissions')

View File

@@ -0,0 +1,3 @@
export { MemoryRoomManager } from '@/rooms/memory-manager'
export { RedisRoomManager } from '@/rooms/redis-manager'
export type { IRoomManager, UserPresence, UserSession, WorkflowRoom } from '@/rooms/types'

View File

@@ -1,6 +1,6 @@
import { createLogger } from '@sim/logger'
import type { Server } from 'socket.io'
import type { IRoomManager, UserPresence, UserSession, WorkflowRoom } from '@/socket/rooms/types'
import type { IRoomManager, UserPresence, UserSession, WorkflowRoom } from '@/rooms/types'
const logger = createLogger('MemoryRoomManager')

View File

@@ -1,7 +1,7 @@
import { createLogger } from '@sim/logger'
import { createClient, type RedisClientType } from 'redis'
import type { Server } from 'socket.io'
import type { IRoomManager, UserPresence, UserSession } from '@/socket/rooms/types'
import type { IRoomManager, UserPresence, UserSession } from '@/rooms/types'
const logger = createLogger('RedisRoomManager')

View File

@@ -1,7 +1,7 @@
import type { IncomingMessage, ServerResponse } from 'http'
import { env } from '@/lib/core/config/env'
import { safeCompare } from '@/lib/core/security/encryption'
import type { IRoomManager } from '@/socket/rooms'
import { safeCompare } from '@sim/security/compare'
import { env } from '@/env'
import type { IRoomManager } from '@/rooms'
interface Logger {
info: (message: string, ...args: unknown[]) => void

View File

@@ -0,0 +1,11 @@
{
"extends": "@sim/tsconfig/base.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,27 @@
import path from 'node:path'
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
globals: true,
environment: 'node',
include: ['**/*.test.{ts,tsx}'],
exclude: ['**/node_modules/**', '**/dist/**'],
setupFiles: ['./vitest.setup.ts'],
pool: 'threads',
testTimeout: 10000,
},
resolve: {
alias: [
{
find: '@sim/db',
replacement: path.resolve(__dirname, '../../packages/db'),
},
{
find: '@sim/logger',
replacement: path.resolve(__dirname, '../../packages/logger/src'),
},
{ find: '@', replacement: path.resolve(__dirname, 'src') },
],
},
})

View File

@@ -0,0 +1,6 @@
process.env.DATABASE_URL ??= 'postgres://localhost/test'
process.env.NODE_ENV ??= 'test'
process.env.BETTER_AUTH_URL ??= 'http://localhost:3000'
process.env.BETTER_AUTH_SECRET ??= 'test-better-auth-secret-at-least-32-chars'
process.env.INTERNAL_API_SECRET ??= 'test-internal-api-secret-at-least-32-chars'
process.env.NEXT_PUBLIC_APP_URL ??= 'http://localhost:3000'

View File

@@ -26,6 +26,13 @@ apps/sim/
└── triggers/ # Trigger definitions
```
The Socket.IO collaborative-canvas server lives in a separate workspace at
`apps/realtime/`. It shares DB + auth with `apps/sim` via the `@sim/*`
packages. Do not add imports from `@/lib/webhooks/providers/*`, `@/executor/*`,
`@/blocks/*`, or `@/tools/*` to any package consumed by `apps/realtime`
those heavyweight registries stay in this app. `apps/realtime` calls back
into this app only over internal HTTP with `INTERNAL_API_SECRET`.
### Feature Organization
Features live under `app/workspace/[workspaceId]/`:

View File

@@ -6,6 +6,7 @@ import type { ComponentType, SVGProps } from 'react'
import {
A2AIcon,
AgentMailIcon,
AgentPhoneIcon,
AgiloftIcon,
AhrefsIcon,
AirtableIcon,
@@ -153,6 +154,7 @@ import {
RootlyIcon,
S3Icon,
SalesforceIcon,
SapS4HanaIcon,
SESIcon,
SearchIcon,
SecretsManagerIcon,
@@ -204,6 +206,7 @@ type IconComponent = ComponentType<SVGProps<SVGSVGElement>>
export const blockTypeToIconMap: Record<string, IconComponent> = {
a2a: A2AIcon,
agentmail: AgentMailIcon,
agentphone: AgentPhoneIcon,
agiloft: AgiloftIcon,
ahrefs: AhrefsIcon,
airtable: AirtableIcon,
@@ -349,6 +352,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
rootly: RootlyIcon,
s3: S3Icon,
salesforce: SalesforceIcon,
sap_s4hana: SapS4HanaIcon,
search: SearchIcon,
secrets_manager: SecretsManagerIcon,
sendgrid: SendgridIcon,

View File

@@ -208,6 +208,113 @@
"integrationTypes": ["email", "communication"],
"tags": ["messaging"]
},
{
"type": "agentphone",
"slug": "agentphone",
"name": "AgentPhone",
"description": "Provision numbers, send SMS and iMessage, and place voice calls with AgentPhone",
"longDescription": "Give your workflow a phone. Provision SMS- and voice-enabled numbers, send messages and tapback reactions, place outbound voice calls, manage conversations and contacts, and track usage — all through a single AgentPhone API key.",
"bgColor": "linear-gradient(135deg, #1a1a1a 0%, #0a2a14 100%)",
"iconName": "AgentPhoneIcon",
"docsUrl": "https://docs.sim.ai/tools/agentphone",
"operations": [
{
"name": "Create Number",
"description": "Provision a new SMS- and voice-enabled phone number"
},
{
"name": "List Numbers",
"description": "List all phone numbers provisioned for this AgentPhone account"
},
{
"name": "Release Number",
"description": "Release (delete) a phone number. This action is irreversible."
},
{
"name": "Get Number Messages",
"description": "Fetch messages received on a specific phone number"
},
{
"name": "Create Call",
"description": "Initiate an outbound voice call from an AgentPhone agent"
},
{
"name": "List Calls",
"description": "List voice calls for this AgentPhone account"
},
{
"name": "Get Call",
"description": "Fetch a call and its full transcript"
},
{
"name": "Get Call Transcript",
"description": "Get the full ordered transcript for a call"
},
{
"name": "List Conversations",
"description": "List conversations (message threads) for this AgentPhone account"
},
{
"name": "Get Conversation",
"description": "Get a conversation along with its recent messages"
},
{
"name": "Update Conversation",
"description": "Update conversation metadata (stored state). Pass null to clear existing metadata."
},
{
"name": "Get Conversation Messages",
"description": "Get paginated messages for a conversation"
},
{
"name": "Send Message",
"description": "Send an outbound SMS or iMessage from an AgentPhone agent"
},
{
"name": "React to Message",
"description": "Send an iMessage tapback reaction to a message (iMessage only)"
},
{
"name": "Create Contact",
"description": "Create a new contact in AgentPhone"
},
{
"name": "List Contacts",
"description": "List contacts for this AgentPhone account"
},
{
"name": "Get Contact",
"description": "Fetch a single contact by ID"
},
{
"name": "Update Contact",
"description": "Update a contact"
},
{
"name": "Delete Contact",
"description": "Delete a contact by ID"
},
{
"name": "Get Usage",
"description": "Retrieve current usage statistics for the AgentPhone account"
},
{
"name": "Get Daily Usage",
"description": "Get a daily breakdown of usage (messages, calls, webhooks) for the last N days"
},
{
"name": "Get Monthly Usage",
"description": "Get monthly usage aggregation (messages, calls, webhooks) for the last N months"
}
],
"operationCount": 22,
"triggers": [],
"triggerCount": 0,
"authType": "api-key",
"category": "tools",
"integrationTypes": ["communication", "developer-tools"],
"tags": ["messaging", "automation"]
},
{
"type": "agiloft",
"slug": "agiloft",
@@ -924,7 +1031,7 @@
},
{
"name": "List Applications",
"description": "Lists all applications in an Ashby organization with pagination and optional filters for status, job, candidate, and creation date."
"description": "Lists all applications in an Ashby organization with pagination and optional filters for status, job, and creation date."
},
{
"name": "Get Application",
@@ -944,11 +1051,11 @@
},
{
"name": "Add Candidate Tag",
"description": "Adds a tag to a candidate in Ashby."
"description": "Adds a tag to a candidate in Ashby and returns the updated candidate."
},
{
"name": "Remove Candidate Tag",
"description": "Removes a tag from a candidate in Ashby."
"description": "Removes a tag from a candidate in Ashby and returns the updated candidate."
},
{
"name": "Get Offer",
@@ -11272,6 +11379,177 @@
"integrationTypes": ["crm", "customer-support", "sales"],
"tags": ["sales-engagement", "customer-support"]
},
{
"type": "sap_s4hana",
"slug": "sap-s-4hana",
"name": "SAP S/4HANA",
"description": "Read and write SAP S/4HANA Cloud business data via OData",
"longDescription": "Connect SAP S/4HANA Cloud Public Edition with per-tenant OAuth 2.0 client credentials configured in your Communication Arrangements. Read and create business partners, customers, suppliers, sales orders, deliveries (inbound/outbound), billing documents, products, stock and material documents, purchase requisitions, purchase orders, and supplier invoices, or run arbitrary OData v2 queries against any whitelisted Communication Scenario.",
"bgColor": "#0A6ED1",
"iconName": "SapS4HanaIcon",
"docsUrl": "https://docs.sim.ai/tools/sap_s4hana",
"operations": [
{
"name": "List Business Partners",
"description": "List business partners from SAP S/4HANA Cloud (API_BUSINESS_PARTNER, A_BusinessPartner) with optional OData $filter, $top, $skip, $orderby, $select, $expand."
},
{
"name": "Get Business Partner",
"description": "Retrieve a single business partner by BusinessPartner key from SAP S/4HANA Cloud (API_BUSINESS_PARTNER, A_BusinessPartner)."
},
{
"name": "Create Business Partner",
"description": "Create a business partner in SAP S/4HANA Cloud (API_BUSINESS_PARTNER, A_BusinessPartner). For Person category 1 provide FirstName and LastName. For Organization category 2 provide OrganizationBPName1."
},
{
"name": "Update Business Partner",
"description": "Update fields on an A_BusinessPartner entity in SAP S/4HANA Cloud (API_BUSINESS_PARTNER). PATCH only sends the fields you provide; existing values are preserved. If-Match defaults to a wildcard (unconditional) — for safe concurrent updates pass the ETag from a prior GET to avoid lost updates."
},
{
"name": "List Customers",
"description": "List customers from SAP S/4HANA Cloud (API_BUSINESS_PARTNER, A_Customer) with optional OData $filter, $top, $skip, $orderby, $select, $expand."
},
{
"name": "Get Customer",
"description": "Retrieve a single customer by Customer key from SAP S/4HANA Cloud (API_BUSINESS_PARTNER, A_Customer)."
},
{
"name": "Update Customer",
"description": "Update fields on an A_Customer entity in SAP S/4HANA Cloud (API_BUSINESS_PARTNER). PATCH only sends the fields you provide; existing values are preserved. A_Customer PATCH is limited to modifiable fields such as OrderIsBlockedForCustomer, DeliveryIsBlock, BillingIsBlockedForCustomer, PostingIsBlocked, and DeletionIndicator. If-Match defaults to a wildcard - for safe concurrent updates pass the ETag from a prior GET to avoid lost updates."
},
{
"name": "List Suppliers",
"description": "List suppliers from SAP S/4HANA Cloud (API_BUSINESS_PARTNER, A_Supplier) with optional OData $filter, $top, $skip, $orderby, $select, $expand."
},
{
"name": "Get Supplier",
"description": "Retrieve a single supplier by Supplier key from SAP S/4HANA Cloud (API_BUSINESS_PARTNER, A_Supplier)."
},
{
"name": "Update Supplier",
"description": "Update fields on an A_Supplier entity in SAP S/4HANA Cloud (API_BUSINESS_PARTNER). PATCH only sends the fields you provide; existing values are preserved. A_Supplier PATCH is limited to modifiable fields such as PostingIsBlocked, PurchasingIsBlocked, PaymentIsBlockedForSupplier, DeletionIndicator, and SupplierAccountGroup. If-Match defaults to a wildcard - for safe concurrent updates pass the ETag from a prior GET to avoid lost updates."
},
{
"name": "List Sales Orders",
"description": "List sales orders from SAP S/4HANA Cloud (API_SALES_ORDER_SRV, A_SalesOrder) with optional OData $filter, $top, $skip, $orderby, $select, $expand."
},
{
"name": "Get Sales Order",
"description": "Retrieve a single sales order by SalesOrder key from SAP S/4HANA Cloud (API_SALES_ORDER_SRV, A_SalesOrder)."
},
{
"name": "Create Sales Order",
"description": "Create a sales order in SAP S/4HANA Cloud (API_SALES_ORDER_SRV, A_SalesOrder) with deep insert of sales order items via to_Item."
},
{
"name": "Update Sales Order",
"description": "Update fields on an A_SalesOrder entity in SAP S/4HANA Cloud (API_SALES_ORDER_SRV). PATCH only sends the fields you provide; existing values are preserved. If-Match defaults to a wildcard (unconditional) — for safe concurrent updates pass the ETag from a prior GET to avoid lost updates."
},
{
"name": "Delete Sales Order",
"description": "Delete an A_SalesOrder entity in SAP S/4HANA Cloud (API_SALES_ORDER_SRV). Only orders without subsequent documents (deliveries, invoices) can be deleted; otherwise reject items via update instead."
},
{
"name": "List Outbound Deliveries",
"description": "List outbound deliveries from SAP S/4HANA Cloud (API_OUTBOUND_DELIVERY_SRV;v=0002, A_OutbDeliveryHeader) with optional OData $filter, $top, $skip, $orderby, $select, $expand."
},
{
"name": "Get Outbound Delivery",
"description": "Retrieve a single outbound delivery by DeliveryDocument key from SAP S/4HANA Cloud (API_OUTBOUND_DELIVERY_SRV;v=0002, A_OutbDeliveryHeader)."
},
{
"name": "List Inbound Deliveries",
"description": "List inbound deliveries from SAP S/4HANA Cloud (API_INBOUND_DELIVERY_SRV;v=0002, A_InbDeliveryHeader) with optional OData $filter, $top, $skip, $orderby, $select, $expand."
},
{
"name": "Get Inbound Delivery",
"description": "Retrieve a single inbound delivery by DeliveryDocument key from SAP S/4HANA Cloud (API_INBOUND_DELIVERY_SRV;v=0002, A_InbDeliveryHeader)."
},
{
"name": "List Billing Documents",
"description": "List billing documents (customer invoices) from SAP S/4HANA Cloud (API_BILLING_DOCUMENT_SRV, A_BillingDocument) with optional OData $filter, $top, $skip, $orderby, $select, $expand."
},
{
"name": "Get Billing Document",
"description": "Retrieve a single billing document (customer invoice) by BillingDocument key from SAP S/4HANA Cloud (API_BILLING_DOCUMENT_SRV, A_BillingDocument)."
},
{
"name": "List Products",
"description": "List products (materials) from SAP S/4HANA Cloud (API_PRODUCT_SRV, A_Product) with optional OData $filter, $top, $skip, $orderby, $select, $expand."
},
{
"name": "Get Product",
"description": "Retrieve a single product (material) by Product key from SAP S/4HANA Cloud (API_PRODUCT_SRV, A_Product)."
},
{
"name": "Update Product",
"description": "Update fields on an A_Product entity in SAP S/4HANA Cloud (API_PRODUCT_SRV). PATCH only sends the fields you provide; existing values are preserved. Flat scalar header fields only — deep/multi-entity updates across navigation properties are not supported by API_PRODUCT_SRV PATCH/PUT (see SAP KBA 2833338); update child entities (plant, valuation, sales data, etc.) via their own endpoints. If-Match defaults to a wildcard (unconditional) — for safe concurrent updates pass the ETag from a prior GET."
},
{
"name": "List Material Stock",
"description": "List material stock quantities from SAP S/4HANA Cloud (API_MATERIAL_STOCK_SRV, A_MatlStkInAcctMod). The entity uses an 11-field composite key (Material, Plant, StorageLocation, Batch, Supplier, Customer, WBSElementInternalID, SDDocument, SDDocumentItem, InventorySpecialStockType, InventoryStockType) — query with $filter on these fields instead of a direct key lookup."
},
{
"name": "List Material Documents",
"description": "List material document headers (goods movements) from SAP S/4HANA Cloud (API_MATERIAL_DOCUMENT_SRV, A_MaterialDocumentHeader) with optional OData $filter, $top, $skip, $orderby, $select, $expand."
},
{
"name": "Get Material Document",
"description": "Retrieve a single material document header by composite key (MaterialDocument + MaterialDocumentYear) from SAP S/4HANA Cloud (API_MATERIAL_DOCUMENT_SRV, A_MaterialDocumentHeader)."
},
{
"name": "List Purchase Requisitions",
"description": "List purchase requisitions from SAP S/4HANA Cloud (API_PURCHASEREQ_PROCESS_SRV, A_PurchaseRequisitionHeader) with optional OData $filter, $top, $skip, $orderby, $select, $expand. Note: API_PURCHASEREQ_PROCESS_SRV is deprecated since S/4HANA Cloud Public Edition 2402; the successor is API_PURCHASEREQUISITION_2 (OData v4). This tool still works against tenants where the legacy service is enabled."
},
{
"name": "Get Purchase Requisition",
"description": "Retrieve a single purchase requisition by PurchaseRequisition key from SAP S/4HANA Cloud (API_PURCHASEREQ_PROCESS_SRV, A_PurchaseRequisitionHeader). Note: API_PURCHASEREQ_PROCESS_SRV is deprecated since S/4HANA Cloud Public Edition 2402; the successor is API_PURCHASEREQUISITION_2 (OData v4). This tool still works against tenants where the legacy service is enabled."
},
{
"name": "Create Purchase Requisition",
"description": "Create a purchase requisition in SAP S/4HANA Cloud (API_PURCHASEREQ_PROCESS_SRV, A_PurchaseRequisitionHeader). PurchaseRequisition is auto-assigned by SAP from the document number range; provide line items via the to_PurchaseReqnItem deep-insert array. Note: API_PURCHASEREQ_PROCESS_SRV is deprecated since S/4HANA Cloud Public Edition 2402; the successor is API_PURCHASEREQUISITION_2 (OData v4). This tool still works against tenants where the legacy service is enabled."
},
{
"name": "Update Purchase Requisition",
"description": "Update fields on an A_PurchaseRequisitionHeader entity in SAP S/4HANA Cloud (API_PURCHASEREQ_PROCESS_SRV; deprecated since S/4HANA 2402, successor is API_PURCHASEREQUISITION_2 OData v4). PATCH only sends the fields you provide; existing values are preserved. If-Match defaults to a wildcard - for safe concurrent updates pass the ETag from a prior GET to avoid lost updates."
},
{
"name": "List Purchase Orders",
"description": "List purchase orders from SAP S/4HANA Cloud (API_PURCHASEORDER_PROCESS_SRV, A_PurchaseOrder) with optional OData $filter, $top, $skip, $orderby, $select, $expand."
},
{
"name": "Get Purchase Order",
"description": "Retrieve a single purchase order by PurchaseOrder key from SAP S/4HANA Cloud (API_PURCHASEORDER_PROCESS_SRV, A_PurchaseOrder)."
},
{
"name": "Create Purchase Order",
"description": "Create a purchase order in SAP S/4HANA Cloud (API_PURCHASEORDER_PROCESS_SRV, A_PurchaseOrder). PurchaseOrder is auto-assigned by SAP from the document number range; provide line items via the body parameter."
},
{
"name": "Update Purchase Order",
"description": "Update fields on an A_PurchaseOrder entity in SAP S/4HANA Cloud (API_PURCHASEORDER_PROCESS_SRV). PATCH only sends the fields you provide; existing values are preserved. If-Match defaults to a wildcard (unconditional) — for safe concurrent updates pass the ETag from a prior GET to avoid lost updates."
},
{
"name": "List Supplier Invoices",
"description": "List supplier invoices from SAP S/4HANA Cloud (API_SUPPLIERINVOICE_PROCESS_SRV, A_SupplierInvoice) with optional OData $filter, $top, $skip, $orderby, $select, $expand."
},
{
"name": "Get Supplier Invoice",
"description": "Retrieve a single supplier invoice by composite key (SupplierInvoice + FiscalYear) from SAP S/4HANA Cloud (API_SUPPLIERINVOICE_PROCESS_SRV, A_SupplierInvoice)."
},
{
"name": "OData Query (advanced)",
"description": "Make an arbitrary OData v2 call against any SAP S/4HANA Cloud whitelisted Communication Scenario. Use when no dedicated tool exists for the entity. The proxy handles auth, CSRF, and OData unwrapping."
}
],
"operationCount": 38,
"triggers": [],
"triggerCount": 0,
"authType": "none",
"category": "tools",
"integrationTypes": ["other", "developer-tools"],
"tags": ["automation"]
},
{
"type": "search",
"slug": "search",
@@ -11876,6 +12154,22 @@
"name": "Create Channel Canvas",
"description": "Create a canvas pinned to a Slack channel as its resource hub"
},
{
"name": "Get Canvas Info",
"description": "Get Slack canvas file metadata by canvas ID"
},
{
"name": "List Canvases",
"description": "List Slack canvases available to the authenticated user or bot"
},
{
"name": "Lookup Canvas Sections",
"description": "Find Slack canvas section IDs matching criteria for later edits"
},
{
"name": "Delete Canvas",
"description": "Delete a Slack canvas by its canvas ID"
},
{
"name": "Create Conversation",
"description": "Create a new public or private channel in a Slack workspace."
@@ -11901,7 +12195,7 @@
"description": "Publish a static view to a user"
}
],
"operationCount": 25,
"operationCount": 29,
"triggers": [
{
"id": "slack_webhook",

View File

@@ -16,6 +16,14 @@ function getMothershipUrl(environment: string): string | null {
return ENV_URLS[environment] ?? null
}
const ENDPOINT_PATTERN = /^[a-zA-Z0-9_-]+(?:\/[a-zA-Z0-9_-]+)*$/
function isValidEndpoint(endpoint: string): boolean {
if (!endpoint) return false
if (endpoint.includes('..')) return false
return ENDPOINT_PATTERN.test(endpoint)
}
async function isAdminRequestAuthorized() {
const session = await getSession()
if (!session?.user?.id) return false
@@ -57,6 +65,10 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
return NextResponse.json({ error: 'endpoint query param required' }, { status: 400 })
}
if (!isValidEndpoint(endpoint)) {
return NextResponse.json({ error: 'invalid endpoint' }, { status: 400 })
}
const baseUrl = getMothershipUrl(environment)
if (!baseUrl) {
return NextResponse.json(
@@ -108,6 +120,10 @@ export const GET = withRouteHandler(async (req: NextRequest) => {
return NextResponse.json({ error: 'endpoint query param required' }, { status: 400 })
}
if (!isValidEndpoint(endpoint)) {
return NextResponse.json({ error: 'invalid endpoint' }, { status: 400 })
}
const baseUrl = getMothershipUrl(environment)
if (!baseUrl) {
return NextResponse.json(

View File

@@ -1,10 +1,10 @@
import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
import { db } from '@sim/db'
import { user } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { auth } from '@/lib/auth'
import { isSameOrigin } from '@/lib/core/utils/validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'

View File

@@ -1,6 +1,7 @@
import { db } from '@sim/db'
import { account, credential, credentialMember } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz'
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
@@ -12,7 +13,6 @@ import {
getCanonicalScopesForProvider,
getServiceAccountProviderForProviderId,
} from '@/lib/oauth/utils'
import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils'
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
export const dynamic = 'force-dynamic'

View File

@@ -23,7 +23,7 @@ vi.mock('@/lib/webhooks/utils.server', () => ({
syncAllWebhooksForCredentialSet: mockSyncAllWebhooksForCredentialSet,
}))
vi.mock('@/lib/audit/log', () => auditMock)
vi.mock('@sim/audit', () => auditMock)
import { POST } from '@/app/api/auth/oauth/disconnect/route'

View File

@@ -1,10 +1,10 @@
import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
import { db } from '@sim/db'
import { account, credentialSet, credentialSetMember } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, eq, like, or } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'

View File

@@ -1,5 +1,6 @@
import crypto from 'crypto'
import { createLogger } from '@sim/logger'
import { safeCompare } from '@sim/security/compare'
import { hmacSha256Hex } from '@sim/security/hmac'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { env } from '@/lib/core/config/env'
@@ -34,13 +35,9 @@ function validateHmac(searchParams: URLSearchParams, clientSecret: string): bool
.map((key) => `${key}=${params[key]}`)
.join('&')
const generatedHmac = crypto.createHmac('sha256', clientSecret).update(message).digest('hex')
const generatedHmac = hmacSha256Hex(message, clientSecret)
try {
return crypto.timingSafeEqual(Buffer.from(hmac, 'hex'), Buffer.from(generatedHmac, 'hex'))
} catch {
return false
}
return safeCompare(hmac, generatedHmac)
}
export const GET = withRouteHandler(async (request: NextRequest) => {

View File

@@ -32,7 +32,9 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
const returnUrl = request.nextUrl.searchParams.get('returnUrl')
if (!shopDomain) {
const returnUrlParam = returnUrl ? encodeURIComponent(returnUrl) : ''
const safeReturnUrl =
returnUrl && isSameOrigin(returnUrl) ? encodeURIComponent(returnUrl) : ''
const returnUrlJsLiteral = JSON.stringify(safeReturnUrl)
return new NextResponse(
`<!DOCTYPE html>
<html>
@@ -120,7 +122,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
</div>
<script>
const returnUrl = '${returnUrlParam}';
const returnUrl = ${returnUrlJsLiteral};
function handleSubmit(e) {
e.preventDefault();
let shop = document.getElementById('shop').value.trim().toLowerCase();

View File

@@ -1,7 +1,7 @@
import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { getCreditBalance } from '@/lib/billing/credits/balance'
import { purchaseCredits } from '@/lib/billing/credits/purchase'

View File

@@ -112,6 +112,16 @@ vi.mock('@/lib/core/storage', () => ({
getStorageMethod: mockGetStorageMethod,
}))
const { mockCheckRateLimitDirect } = vi.hoisted(() => ({
mockCheckRateLimitDirect: vi.fn(),
}))
vi.mock('@/lib/core/rate-limiter', () => ({
RateLimiter: class {
checkRateLimitDirect = mockCheckRateLimitDirect
},
}))
vi.mock('@/lib/messaging/email/mailer', () => ({
sendEmail: mockSendEmail,
}))
@@ -234,6 +244,13 @@ describe('Chat OTP API Route', () => {
}))
requestUtilsMockFns.mockGenerateRequestId.mockReturnValue('req-123')
requestUtilsMockFns.mockGetClientIp.mockReturnValue('1.2.3.4')
mockCheckRateLimitDirect.mockResolvedValue({
allowed: true,
remaining: 10,
resetAt: new Date(Date.now() + 60_000),
})
mockZodParse.mockImplementation((data: unknown) => data)
@@ -283,6 +300,134 @@ describe('Chat OTP API Route', () => {
})
})
describe('POST - Rate limiting', () => {
const buildDeploymentSelect = () =>
mockDbSelect.mockImplementationOnce(() => ({
from: vi.fn().mockReturnValue({
where: vi.fn().mockReturnValue({
limit: vi.fn().mockResolvedValue([
{
id: mockChatId,
authType: 'email',
allowedEmails: [mockEmail],
title: 'Test Chat',
},
]),
}),
}),
}))
it('returns 429 with Retry-After when IP rate limit is exceeded', async () => {
mockCheckRateLimitDirect.mockResolvedValueOnce({
allowed: false,
remaining: 0,
resetAt: new Date(Date.now() + 900_000),
retryAfterMs: 900_000,
})
const headerSet = vi.fn()
mockCreateErrorResponse.mockImplementationOnce((message: string, status: number) => ({
json: () => Promise.resolve({ error: message }),
status,
headers: { set: headerSet },
}))
const request = new NextRequest('http://localhost:3000/api/chat/test/otp', {
method: 'POST',
body: JSON.stringify({ email: mockEmail }),
})
const response = await POST(request, {
params: Promise.resolve({ identifier: mockIdentifier }),
})
expect(response.status).toBe(429)
expect(headerSet).toHaveBeenCalledWith('Retry-After', '900')
expect(mockSendEmail).not.toHaveBeenCalled()
expect(mockDbSelect).not.toHaveBeenCalled()
})
it('returns 429 with Retry-After when email rate limit is exceeded', async () => {
mockCheckRateLimitDirect
.mockResolvedValueOnce({
allowed: true,
remaining: 9,
resetAt: new Date(Date.now() + 60_000),
})
.mockResolvedValueOnce({
allowed: false,
remaining: 0,
resetAt: new Date(Date.now() + 900_000),
retryAfterMs: 900_000,
})
const headerSet = vi.fn()
mockCreateErrorResponse.mockImplementationOnce((message: string, status: number) => ({
json: () => Promise.resolve({ error: message }),
status,
headers: { set: headerSet },
}))
buildDeploymentSelect()
const request = new NextRequest('http://localhost:3000/api/chat/test/otp', {
method: 'POST',
body: JSON.stringify({ email: mockEmail }),
})
const response = await POST(request, {
params: Promise.resolve({ identifier: mockIdentifier }),
})
expect(response.status).toBe(429)
expect(headerSet).toHaveBeenCalledWith('Retry-After', '900')
expect(mockSendEmail).not.toHaveBeenCalled()
})
it('falls back to refill interval when retryAfterMs is missing', async () => {
mockCheckRateLimitDirect.mockResolvedValueOnce({
allowed: false,
remaining: 0,
resetAt: new Date(Date.now() + 900_000),
})
const headerSet = vi.fn()
mockCreateErrorResponse.mockImplementationOnce((message: string, status: number) => ({
json: () => Promise.resolve({ error: message }),
status,
headers: { set: headerSet },
}))
const request = new NextRequest('http://localhost:3000/api/chat/test/otp', {
method: 'POST',
body: JSON.stringify({ email: mockEmail }),
})
await POST(request, { params: Promise.resolve({ identifier: mockIdentifier }) })
expect(headerSet).toHaveBeenCalledWith('Retry-After', '900')
})
it('skips IP rate limit when client IP is unknown', async () => {
requestUtilsMockFns.mockGetClientIp.mockReturnValueOnce('unknown')
buildDeploymentSelect()
const request = new NextRequest('http://localhost:3000/api/chat/test/otp', {
method: 'POST',
body: JSON.stringify({ email: mockEmail }),
})
await POST(request, { params: Promise.resolve({ identifier: mockIdentifier }) })
// Only the email-scoped check should run, not the IP-scoped one
expect(mockCheckRateLimitDirect).toHaveBeenCalledTimes(1)
expect(mockCheckRateLimitDirect).toHaveBeenCalledWith(
expect.stringContaining('chat-otp:email:'),
expect.any(Object)
)
})
})
describe('POST - Store OTP (Database path)', () => {
beforeEach(() => {
mockGetStorageMethod.mockReturnValue('database')

View File

@@ -8,9 +8,11 @@ import type { NextRequest } from 'next/server'
import { z } from 'zod'
import { renderOTPEmail } from '@/components/emails'
import { getRedisClient } from '@/lib/core/config/redis'
import type { TokenBucketConfig } from '@/lib/core/rate-limiter'
import { RateLimiter } from '@/lib/core/rate-limiter'
import { addCorsHeaders, isEmailAllowed } from '@/lib/core/security/deployment'
import { getStorageMethod } from '@/lib/core/storage'
import { generateRequestId } from '@/lib/core/utils/request'
import { generateRequestId, getClientIp } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { sendEmail } from '@/lib/messaging/email/mailer'
import { setChatAuthCookie } from '@/app/api/chat/utils'
@@ -18,6 +20,20 @@ import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/
const logger = createLogger('ChatOtpAPI')
const rateLimiter = new RateLimiter()
const OTP_IP_RATE_LIMIT: TokenBucketConfig = {
maxTokens: 10,
refillRate: 10,
refillIntervalMs: 15 * 60_000,
}
const OTP_EMAIL_RATE_LIMIT: TokenBucketConfig = {
maxTokens: 3,
refillRate: 3,
refillIntervalMs: 15 * 60_000,
}
function generateOTP(): string {
return randomInt(100000, 1000000).toString()
}
@@ -214,6 +230,23 @@ export const POST = withRouteHandler(
const requestId = generateRequestId()
try {
const ip = getClientIp(request)
if (ip !== 'unknown') {
const ipRateLimit = await rateLimiter.checkRateLimitDirect(
`chat-otp:ip:${identifier}:${ip}`,
OTP_IP_RATE_LIMIT
)
if (!ipRateLimit.allowed) {
logger.warn(`[${requestId}] OTP IP rate limit exceeded for ${identifier} from ${ip}`)
const retryAfter = Math.ceil(
(ipRateLimit.retryAfterMs ?? OTP_IP_RATE_LIMIT.refillIntervalMs) / 1000
)
const response = createErrorResponse('Too many requests. Please try again later.', 429)
response.headers.set('Retry-After', String(retryAfter))
return addCorsHeaders(response, request)
}
}
const body = await request.json()
const { email } = otpRequestSchema.parse(body)
@@ -255,6 +288,25 @@ export const POST = withRouteHandler(
)
}
const emailRateLimit = await rateLimiter.checkRateLimitDirect(
`chat-otp:email:${deployment.id}:${email.toLowerCase()}`,
OTP_EMAIL_RATE_LIMIT
)
if (!emailRateLimit.allowed) {
logger.warn(
`[${requestId}] OTP email rate limit exceeded for ${email} on chat ${deployment.id}`
)
const retryAfter = Math.ceil(
(emailRateLimit.retryAfterMs ?? OTP_EMAIL_RATE_LIMIT.refillIntervalMs) / 1000
)
const response = createErrorResponse(
'Too many verification code requests. Please try again later.',
429
)
response.headers.set('Retry-After', String(retryAfter))
return addCorsHeaders(response, request)
}
const otp = generateOTP()
await storeOTP(email, deployment.id, otp)

View File

@@ -33,7 +33,7 @@ const mockPerformChatUndeploy = workflowsOrchestrationMockFns.mockPerformChatUnd
const mockNotifySocketDeploymentChanged =
workflowsOrchestrationMockFns.mockNotifySocketDeploymentChanged
vi.mock('@/lib/audit/log', () => auditMock)
vi.mock('@sim/audit', () => auditMock)
vi.mock('@/lib/core/config/feature-flags', () => ({
isDev: true,
isHosted: false,

View File

@@ -1,10 +1,10 @@
import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
import { db } from '@sim/db'
import { chat } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, eq, isNull } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { z } from 'zod'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { isDev } from '@/lib/core/config/feature-flags'
import { encryptSecret } from '@/lib/core/security/encryption'

View File

@@ -40,7 +40,7 @@ vi.mock('@/serializer', () => ({
Serializer: vi.fn(),
}))
vi.mock('@/lib/workflows/subblocks', () => ({
vi.mock('@sim/workflow-persistence/subblocks', () => ({
mergeSubblockStateWithValues: mockMergeSubblockStateWithValues,
mergeSubBlockValues: mockMergeSubBlockValues,
}))

View File

@@ -1,6 +1,7 @@
import { db } from '@sim/db'
import { chat, workflow } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz'
import { and, eq, isNull } from 'drizzle-orm'
import type { NextRequest, NextResponse } from 'next/server'
import {
@@ -9,7 +10,6 @@ import {
validateAuthToken,
} from '@/lib/core/security/deployment'
import { decryptSecret } from '@/lib/core/security/encryption'
import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils'
const logger = createLogger('ChatAuthUtils')

View File

@@ -2,6 +2,7 @@ import { db } from '@sim/db'
import { copilotChats } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz'
import { and, desc, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getLatestRunForStream } from '@/lib/copilot/async-runs/repository'
@@ -17,7 +18,6 @@ import {
import { readFilePreviewSessions } from '@/lib/copilot/request/session'
import { readEvents } from '@/lib/copilot/request/session/buffer'
import { toStreamBatchEvent } from '@/lib/copilot/request/session/types'
import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils'
import { assertActiveWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
const logger = createLogger('CopilotChatAPI')

View File

@@ -52,6 +52,8 @@ const ContentBlockSchema = z.object({
lifecycle: z.enum(['start', 'end']).optional(),
status: z.enum(['complete', 'error', 'cancelled']).optional(),
toolCall: StoredToolCallSchema.optional(),
timestamp: z.number().optional(),
endedAt: z.number().optional(),
})
const StopSchema = z.object({

View File

@@ -38,6 +38,7 @@ vi.mock('@/lib/copilot/request/session', () => ({
}),
encodeSSEEnvelope: (event: Record<string, unknown>) =>
new TextEncoder().encode(`data: ${JSON.stringify(event)}\n\n`),
encodeSSEComment: (comment: string) => new TextEncoder().encode(`: ${comment}\n\n`),
SSE_RESPONSE_HEADERS: {
'Content-Type': 'text/event-stream',
},
@@ -132,6 +133,7 @@ describe('copilot chat stream replay route', () => {
)
const chunks = await readAllChunks(response)
expect(chunks[0]).toBe(': accepted\n\n')
expect(chunks.join('')).toContain(
JSON.stringify({
status: MothershipStreamV1CompletionStatus.cancelled,

View File

@@ -1,5 +1,6 @@
import { context as otelContext, trace } from '@opentelemetry/api'
import { createLogger } from '@sim/logger'
import { sleep } from '@sim/utils/helpers'
import { type NextRequest, NextResponse } from 'next/server'
import { getLatestRunForStream } from '@/lib/copilot/async-runs/repository'
import {
@@ -18,6 +19,7 @@ import { getCopilotTracer, markSpanForError } from '@/lib/copilot/request/otel'
import {
checkForReplayGap,
createEvent,
encodeSSEComment,
encodeSSEEnvelope,
readEvents,
readFilePreviewSessions,
@@ -30,6 +32,7 @@ export const maxDuration = 3600
const logger = createLogger('CopilotChatStreamAPI')
const POLL_INTERVAL_MS = 250
const REPLAY_KEEPALIVE_INTERVAL_MS = 15_000
const MAX_STREAM_MS = 60 * 60 * 1000
function extractCanonicalRequestId(value: unknown): string {
@@ -265,6 +268,7 @@ async function handleResumeRequestBody({
let controllerClosed = false
let sawTerminalEvent = false
let currentRequestId = extractRunRequestId(run)
let lastWriteTime = Date.now()
// Stamp the logical request id + chat id on the resume root as soon
// as we resolve them from the run row, so TraceQL joins work on
// resume legs the same way they do on the original POST.
@@ -290,6 +294,19 @@ async function handleResumeRequestBody({
if (controllerClosed) return false
try {
controller.enqueue(encodeSSEEnvelope(payload))
lastWriteTime = Date.now()
return true
} catch {
controllerClosed = true
return false
}
}
const enqueueComment = (comment: string) => {
if (controllerClosed) return false
try {
controller.enqueue(encodeSSEComment(comment))
lastWriteTime = Date.now()
return true
} catch {
controllerClosed = true
@@ -305,7 +322,6 @@ async function handleResumeRequestBody({
const flushEvents = async () => {
const events = await readEvents(streamId, cursor)
if (events.length > 0) {
totalEventsFlushed += events.length
logger.debug('[Resume] Flushing events', {
streamId,
afterCursor: cursor,
@@ -313,14 +329,15 @@ async function handleResumeRequestBody({
})
}
for (const envelope of events) {
if (!enqueueEvent(envelope)) {
break
}
totalEventsFlushed += 1
cursor = envelope.stream.cursor ?? String(envelope.seq)
currentRequestId = extractEnvelopeRequestId(envelope) || currentRequestId
if (envelope.type === MothershipStreamV1EventType.complete) {
sawTerminalEvent = true
}
if (!enqueueEvent(envelope)) {
break
}
}
}
@@ -340,21 +357,30 @@ async function handleResumeRequestBody({
reason: options?.reason,
requestId: currentRequestId,
})) {
if (!enqueueEvent(envelope)) {
break
}
cursor = envelope.stream.cursor ?? String(envelope.seq)
if (envelope.type === MothershipStreamV1EventType.complete) {
sawTerminalEvent = true
}
if (!enqueueEvent(envelope)) {
break
}
}
}
try {
enqueueComment('accepted')
const gap = await checkForReplayGap(streamId, afterCursor, currentRequestId)
if (gap) {
for (const envelope of gap.envelopes) {
enqueueEvent(envelope)
if (!enqueueEvent(envelope)) {
break
}
cursor = envelope.stream.cursor ?? String(envelope.seq)
currentRequestId = extractEnvelopeRequestId(envelope) || currentRequestId
if (envelope.type === MothershipStreamV1EventType.complete) {
sawTerminalEvent = true
}
}
return
}
@@ -407,7 +433,11 @@ async function handleResumeRequestBody({
break
}
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS))
if (Date.now() - lastWriteTime >= REPLAY_KEEPALIVE_INTERVAL_MS) {
enqueueComment('keepalive')
}
await sleep(POLL_INTERVAL_MS)
}
if (!controllerClosed && Date.now() - startTime >= MAX_STREAM_MS) {
emitTerminalIfMissing(MothershipStreamV1CompletionStatus.error, {

View File

@@ -1,6 +1,7 @@
import { db } from '@sim/db'
import { copilotChats, permissions, workflow, workspace } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz'
import { and, desc, eq, isNull, or, sql } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
@@ -13,7 +14,6 @@ import {
} from '@/lib/copilot/request/http'
import { taskPubSub } from '@/lib/copilot/tasks'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils'
import { assertActiveWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
const logger = createLogger('CopilotChatsListAPI')

View File

@@ -3,7 +3,7 @@
*
* @vitest-environment node
*/
import { authMockFns, workflowsUtilsMock, workflowsUtilsMockFns } from '@sim/testing'
import { authMockFns, workflowAuthzMockFns, workflowsUtilsMock } from '@sim/testing'
import { NextRequest } from 'next/server'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
@@ -63,7 +63,7 @@ describe('Copilot Checkpoints Revert API Route', () => {
authMockFns.mockGetSession.mockResolvedValue(null)
workflowsUtilsMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({
workflowAuthzMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({
allowed: true,
status: 200,
})
@@ -251,7 +251,7 @@ describe('Copilot Checkpoints Revert API Route', () => {
thenResults.push(mockCheckpoint) // Checkpoint found
thenResults.push(mockWorkflow) // Workflow found but different user
workflowsUtilsMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValueOnce({
workflowAuthzMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValueOnce({
allowed: false,
status: 403,
})

View File

@@ -1,6 +1,7 @@
import { db } from '@sim/db'
import { workflowCheckpoints, workflow as workflowTable } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz'
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
@@ -14,7 +15,6 @@ import {
} from '@/lib/copilot/request/http'
import { getInternalApiBaseUrl } from '@/lib/core/utils/urls'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils'
import { isUuidV4 } from '@/executor/constants'
const logger = createLogger('CheckpointRevertAPI')

View File

@@ -3,7 +3,7 @@
*
* @vitest-environment node
*/
import { authMockFns, workflowsUtilsMock, workflowsUtilsMockFns } from '@sim/testing'
import { authMockFns, workflowAuthzMockFns, workflowsUtilsMock } from '@sim/testing'
import { NextRequest } from 'next/server'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
@@ -79,7 +79,7 @@ describe('Copilot Checkpoints API Route', () => {
userId: 'user-123',
workflowId: 'workflow-123',
})
workflowsUtilsMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({
workflowAuthzMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({
allowed: true,
})
})

View File

@@ -1,6 +1,7 @@
import { db } from '@sim/db'
import { workflowCheckpoints } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz'
import { and, desc, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
@@ -13,7 +14,6 @@ import {
createUnauthorizedResponse,
} from '@/lib/copilot/request/http'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils'
const logger = createLogger('WorkflowCheckpointsAPI')

View File

@@ -1,10 +1,7 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import {
authenticateCopilotRequestSessionOnly,
createUnauthorizedResponse,
} from '@/lib/copilot/request/http'
import { checkInternalApiKey, createUnauthorizedResponse } from '@/lib/copilot/request/http'
import { env } from '@/lib/core/config/env'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
@@ -21,8 +18,8 @@ const TrainingExampleSchema = z.object({
})
export const POST = withRouteHandler(async (request: NextRequest) => {
const { userId, isAuthenticated } = await authenticateCopilotRequestSessionOnly()
if (!isAuthenticated || !userId) {
const auth = checkInternalApiKey(request)
if (!auth.success) {
return createUnauthorizedResponse()
}

View File

@@ -1,10 +1,7 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import {
authenticateCopilotRequestSessionOnly,
createUnauthorizedResponse,
} from '@/lib/copilot/request/http'
import { checkInternalApiKey, createUnauthorizedResponse } from '@/lib/copilot/request/http'
import { env } from '@/lib/core/config/env'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
@@ -27,8 +24,8 @@ const TrainingDataSchema = z.object({
})
export const POST = withRouteHandler(async (request: NextRequest) => {
const { userId, isAuthenticated } = await authenticateCopilotRequestSessionOnly()
if (!isAuthenticated || !userId) {
const auth = checkInternalApiKey(request)
if (!auth.success) {
return createUnauthorizedResponse()
}

View File

@@ -1,10 +1,10 @@
import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
import { db } from '@sim/db'
import { credentialSet, credentialSetInvitation, member, organization, user } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getEmailSubject, renderPollingGroupInvitationEmail } from '@/components/emails'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { hasCredentialSetsAccess } from '@/lib/billing'
import { getBaseUrl } from '@/lib/core/utils/urls'

View File

@@ -1,3 +1,4 @@
import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
import { db } from '@sim/db'
import { credentialSet, credentialSetInvitation, member, organization, user } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
@@ -6,7 +7,6 @@ import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getEmailSubject, renderPollingGroupInvitationEmail } from '@/components/emails'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { hasCredentialSetsAccess } from '@/lib/billing'
import { getBaseUrl } from '@/lib/core/utils/urls'

View File

@@ -1,10 +1,10 @@
import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
import { db } from '@sim/db'
import { account, credentialSet, credentialSetMember, member, user } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { generateId } from '@sim/utils/id'
import { and, eq, inArray } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { hasCredentialSetsAccess } from '@/lib/billing'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'

View File

@@ -1,10 +1,10 @@
import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
import { db } from '@sim/db'
import { credentialSet, credentialSetMember, member } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { hasCredentialSetsAccess } from '@/lib/billing'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'

View File

@@ -1,3 +1,4 @@
import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
import { db } from '@sim/db'
import {
credentialSet,
@@ -9,9 +10,9 @@ import { createLogger } from '@sim/logger'
import { generateId } from '@sim/utils/id'
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { normalizeEmail } from '@/lib/invitations/core'
import { syncAllWebhooksForCredentialSet } from '@/lib/webhooks/utils.server'
const logger = createLogger('CredentialSetInviteToken')
@@ -111,6 +112,21 @@ export const POST = withRouteHandler(
return NextResponse.json({ error: 'Invitation has expired' }, { status: 410 })
}
if (invitation.email) {
const sessionEmail = session.user.email
if (!sessionEmail || normalizeEmail(sessionEmail) !== normalizeEmail(invitation.email)) {
logger.warn('Rejected credential set invitation accept due to email mismatch', {
invitationId: invitation.id,
credentialSetId: invitation.credentialSetId,
userId: session.user.id,
})
return NextResponse.json(
{ error: 'This invitation was sent to a different email address' },
{ status: 403 }
)
}
}
const existingMember = await db
.select()
.from(credentialSetMember)

View File

@@ -1,10 +1,10 @@
import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
import { db } from '@sim/db'
import { credentialSet, credentialSetMember, organization } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { generateId } from '@sim/utils/id'
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { syncAllWebhooksForCredentialSet } from '@/lib/webhooks/utils.server'

View File

@@ -1,3 +1,4 @@
import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
import { db } from '@sim/db'
import { credentialSet, credentialSetMember, member, organization, user } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
@@ -5,7 +6,6 @@ import { generateId } from '@sim/utils/id'
import { and, count, desc, eq } from 'drizzle-orm'
import { NextResponse } from 'next/server'
import { z } from 'zod'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { hasCredentialSetsAccess } from '@/lib/billing'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'

View File

@@ -1,3 +1,4 @@
import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
import { db } from '@sim/db'
import { credential, credentialMember, environment, workspaceEnvironment } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
@@ -5,7 +6,6 @@ import { generateId } from '@sim/utils/id'
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { encryptSecret } from '@/lib/core/security/encryption'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'

View File

@@ -1,3 +1,4 @@
import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
import { db } from '@sim/db'
import { account, credential, credentialMember, workspace } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
@@ -5,7 +6,6 @@ import { generateId } from '@sim/utils/id'
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { encryptSecret } from '@/lib/core/security/encryption'
import { generateRequestId } from '@/lib/core/utils/request'

View File

@@ -1,3 +1,4 @@
import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
import { db } from '@sim/db'
import { environment } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
@@ -5,7 +6,6 @@ import { generateId } from '@sim/utils/id'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { decryptSecret, encryptSecret } from '@/lib/core/security/encryption'
import { generateRequestId } from '@/lib/core/utils/request'

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