Skip to content

feat(core): published definitions are integration types like code; code can embed one; workspaces own their own - #696

Open
seanspeaks wants to merge 11 commits into
claude/shared-conversation-8hgmbr-stack-4-sync-and-paused-runsfrom
claude/shared-conversation-8hgmbr-stack-5-definitions-as-types
Open

seanspeaks wants to merge 11 commits into
claude/shared-conversation-8hgmbr-stack-4-sync-and-paused-runsfrom
claude/shared-conversation-8hgmbr-stack-5-definitions-as-types

Conversation

@seanspeaks

@seanspeaks seanspeaks commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Stack 5 of 9. Stacked on #695.

Summary

One kind of integration. A published definition is installed, listed, named and routed exactly like a code integration. Only the deployment underneath is shared.

  • integration-types.js: resolveIntegrationClass / listIntegrationClasses. A registered class can be a provider (static IntegrationProvider).
  • The integration use cases and the queue worker resolve config.type through it. GET /api/integrations lists code and published types together.
  • IntegrationBase.getDeploymentName(): a hosted class uses its host's queue and schedules but logs under its own name.
  • Each installation's webhook URL is its own type's: /api/{integrationType}/webhooks[/{id}]. The routes are added in devtools.
  • The upgrade, runs, syncs and decision routes now answer for every integration.
  • Publishing moved to /api/admin/integrations/versions. The unreleased /api/admin/data-defined paths are removed.
  • Fix: an approval step's named approvers can decide without owning the installation. Anyone else gets a 404.

Code integrations embed a definition.

  • extendIntegrationClass(CodeClass, { definition, manifests }) gives a code integration workflows, pauses, syncs, UI surfaces, the generated form and run history.
  • Code steps can call the class's own methods with ref #method.
  • The dispatcher records the class's own queued events as runs (triggerType: "code").

A workspace's own integrations.

  • IntegrationDefinitionVersion.workspaceId, with migration 20261019120000_definition_workspace.
  • POST /api/integration-definitions publishes into the user's workspace.
  • GET /api/integration-definitions/:id serves the editor, and only to the owning workspace.
  • An id has one owner, so a taken id gets 409 ID_TAKEN.
  • Option lists and CreateIntegration show the catalogue plus the user's own integrations. Options for the user's own carry editable: true.

ADR-063 is marked accepted, with the route table updated.

Test plan

  • Same 31 pre-existing failures as next at this commit. Nothing new fails.
  • Mutation tests for provider resolution, the list filter, workspace visibility, ID_TAKEN and runs recorded twice.

Generated by Claude Code


Note

High Risk
Changes core integration resolution, multi-tenant definition ownership, webhook routing, and authorization on approval decisions—mistakes could leak ids, block installs, or mis-route webhooks and queue work.

Overview
Published integration definitions behave like first-class integration types: integration-types.js resolves config.type via IntegrationProvider, lists them beside code on GET /api/integrations, and routes queue/schedules/webhooks through the host deployment while keeping each type’s own name in URLs and telemetry (getDeploymentName).

extendIntegrationClass lets hand-written integrations embed a definition (workflows, syncs, UI, form, run history); #method code steps call class methods, and dispatchJob records native queued events as runs when runEvent exists.

Workspace-scoped authoring adds workspaceId (DB migration), POST/GET /api/integration-definitions, catalogue filtering, and ID_TAKEN / 404 rules so tenants cannot probe foreign ids. Admin publish paths move to /admin/integrations/versions; shared routes (runs, syncs, upgrade, approval decisions) return empty/safe answers for code integrations too. Approvers named on a run may decide without owning the install; others get 404. Provider hosts gain dynamic webhook routes and multi-type trigger ticks with continuation cursors.

Reviewed by Cursor Bugbot for commit 5d9f1be. Bugbot is set up for automated code reviews on this repo. Configure here.

📦 Published PR as canary version: 2.0.0--canary.696.5d9f1be.0

✨ Test out this PR locally via:

npm install @friggframework/admin-scripts@2.0.0--canary.696.5d9f1be.0
npm install @friggframework/core@2.0.0--canary.696.5d9f1be.0
npm install @friggframework/devtools@2.0.0--canary.696.5d9f1be.0
npm install @friggframework/eslint-config@2.0.0--canary.696.5d9f1be.0
npm install @friggframework/prettier-config@2.0.0--canary.696.5d9f1be.0
npm install @friggframework/schemas@2.0.0--canary.696.5d9f1be.0
npm install @friggframework/serverless-plugin@2.0.0--canary.696.5d9f1be.0
npm install @friggframework/test@2.0.0--canary.696.5d9f1be.0
npm install @friggframework/ui@2.0.0--canary.696.5d9f1be.0
# or 
yarn add @friggframework/admin-scripts@2.0.0--canary.696.5d9f1be.0
yarn add @friggframework/core@2.0.0--canary.696.5d9f1be.0
yarn add @friggframework/devtools@2.0.0--canary.696.5d9f1be.0
yarn add @friggframework/eslint-config@2.0.0--canary.696.5d9f1be.0
yarn add @friggframework/prettier-config@2.0.0--canary.696.5d9f1be.0
yarn add @friggframework/schemas@2.0.0--canary.696.5d9f1be.0
yarn add @friggframework/serverless-plugin@2.0.0--canary.696.5d9f1be.0
yarn add @friggframework/test@2.0.0--canary.696.5d9f1be.0
yarn add @friggframework/ui@2.0.0--canary.696.5d9f1be.0

…ike code

A published definition is now installed, listed, named and routed exactly as a
code integration; only the deployment underneath is shared.

- integration-types.js: resolveIntegrationClass / listIntegrationClasses. A
  registered class may be a provider (static IntegrationProvider); the shared
  runtime class provides one generated class per published definition, named
  by its id. Registered classes win over a definition with the same name.
- Integration use cases (create, get, update, delete, instance, options) and
  the queue worker resolve config.type through it, so { type: '<id>' }
  installs and pins the latest version, and GET /api/integrations lists code
  and published types in one list.
- IntegrationBase.getDeploymentName(): a hosted class queues and schedules on
  its host's queue while logs, telemetry and job ids carry its own name.
- Webhooks: an installation's URL is its own type's; the host's webhook
  function also serves /api/{integrationType}/webhooks[/{id}] (devtools adds
  the routes; registered integrations keep theirs). App-level webhooks fan
  out to installations of every provided type.
- Upgrade, runs, syncs and decision routes answer for every integration (an
  empty answer for code, not a 404). Publishing moves to
  /api/admin/integrations/versions; the old paths still answer.
- Fix: an approval step's named approvers can decide without owning the
  installation; anyone else gets a 404.
- ADR-063 accepted; route table, shapes and the v0.1 note updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NGC9SnFuqdjX2xH2qeu8vw
…aths

Never on next or published, so nothing depends on them; publishing lives at
/api/admin/integrations/versions only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NGC9SnFuqdjX2xH2qeu8vw
… runtime

extendIntegrationClass(CodeClass, { definition, manifests }) gives a
hand-written integration what published ones have: workflows with delay
and approval pauses, syncs, UI surfaces, the generated configure form and
run history, through the same records and routes.

- The class keeps its name, modules, routes, schedules, lifecycle and
  handlers; the definition's id must be its name and it is validated at
  class creation.
- Code steps may call the class's own methods with ref "#method"; a
  published definition cannot (validate's codeMethods option).
- Webhooks, the configure form and schedules stay the code's unless the
  definition declares them; code schedules run beside compiled ones.
- IntegrationEventDispatcher.dispatchJob calls runEvent when an
  integration has one: the class's own queued events are recorded as runs
  (triggerType "code"); compiled workflows record only their own run. The
  history never decides the handler's outcome.
- The embedded definition ships with the code: installations run the
  deployed version and upgrade reports there is nothing to move to.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NGC9SnFuqdjX2xH2qeu8vw
…edit

The Integrator's "build your own": a signed-in user publishes an
integration into their workspace, where it is listed and installed like any
other, and no one else can see it.

- IntegrationDefinitionVersion.workspaceId (both schemas; PostgreSQL
  migration 20261019120000_definition_workspace). Null is the app's own
  catalogue.
- POST /api/integration-definitions publishes (or drafts) into the user's
  workspace; GET /api/integration-definitions/:id returns every version and
  the newest document for the editor, to its owner only.
- An id has one owner: another workspace's, the catalogue's or a code
  integration's name is refused with 409 ID_TAKEN (admin publishing too).
- resolveIntegrationClass / listIntegrationClasses take the asker's
  workspace: GET /api/integrations options and CreateIntegration see only
  the catalogue plus their own; hydrating an existing installation still
  resolves by id. An own integration's option carries editable: true.
- CreateIntegration refuses a type the user cannot install before writing a
  record.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NGC9SnFuqdjX2xH2qeu8vw

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Code review skipped — your organization has no extra usage available to pay for this review.

If your organization's extra usage balance is empty, an organization admin can add extra usage credits at claude.ai/admin-settings/usage. If its monthly spend limit was reached, an admin can raise it on the same page. If neither applies, contact Anthropic support.

Once extra usage is available, someone with write access to this repository can comment @claude review on this pull request to trigger a review.

@seanspeaks seanspeaks added release Create a release when this pr is merged prerelease This change is available in a prerelease. labels Oct 19, 2026 — with Claude

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread packages/core/integrations/data-defined/data-defined-integration.js
* X-Hook-Secret), otherwise queue the delivery as Frigg always does. The URL names the module (?module=).
*/
async onWebhookReceived({ req, res }) {
if (embedded && !ownsWebhooks(this.def)) return super.onWebhookReceived({ req, res });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Embedded webhook handshake never runs

High Severity

Receipt constructs a dry instance and never calls initialize, so this.def is unset. For an embedded class, ownsWebhooks(this.def) is therefore always false and onWebhookReceived always delegates to super, skipping Slack/Asana-style handshakes even when the embedded definition owns webhooks.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit eb2b29b. Configure here.

Comment thread packages/core/integrations/data-defined/router.js
@seanspeaks

Copy link
Copy Markdown
Contributor Author

ADR reconciliation needed after Stack A/B — drafted with Claude Code at Sean's request

Severity: must-reconcile.

Merge order: #671 (registry) first; whichever of Stack A (#686) and Stack B lands second rebases over the shared files.

…ations

Move publish, history and deprecate from /api/admin/integrations to
/admin/integrations (versions, :id/versions, :id/versions/:v/deprecate), beside
/admin/scripts behind the same admin-auth middleware. Collect every route the
definitions router serves into DEFINITION_ROUTES (method, path, auth) so moving
the user routes onto the /api/v2 registry (#671) is mechanical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NGC9SnFuqdjX2xH2qeu8vw
- Revert the status flip: Accepted is the merge of the ADR PR (ADR-043).
- Route table: note that the routes move under /api/v2 through the #671
  route registry (ADR-053), and that "workspace" is user.getId() for now.
- Use a generic example id in prose.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NGC9SnFuqdjX2xH2qeu8vw
…en code shadows a definition

Publishing an id another workspace owns answered 409 ID_TAKEN, telling one
tenant that another's id exists. It now answers 404 NOT_FOUND, identical to
reading it (ADR-052 section 3). Catalogue and code-integration ids are public
and keep 409 ID_TAKEN; the admin route is not a tenant and keeps it too.

A definition ignored because registered code has the same name is logged once
per process as frigg.integrations.definition.shadowed_by_code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NGC9SnFuqdjX2xH2qeu8vw
…-4, triggers; definitions-as-types moved onto triggers)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NGC9SnFuqdjX2xH2qeu8vw
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NGC9SnFuqdjX2xH2qeu8vw
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NGC9SnFuqdjX2xH2qeu8vw
…e triggers model carried into stack-5's additions

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NGC9SnFuqdjX2xH2qeu8vw
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
C Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

There are 3 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 5d9f1be. Configure here.

if (!latest) return null;
cls = classFor({ ...latest, workspaceId: (await definitions.ownerOf?.(type)) ?? null });
}
return visible(cls, context) ? cls : null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deprecated definitions fail to resolve

High Severity

IntegrationProvider.resolve loads a type only through definitions.latest(), which returns a PUBLISHED document. After every version is DEPRECATED, that call is null, so existing installations of { type: '<id>' } no longer hydrate. The same list()/PUBLISHED-only view drives triggerTypes(), so those installations also drop off the host tick. Deprecation is supposed to block new installs while pinned rows keep running.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5d9f1be. Configure here.

let out;
try { out = await invoke(); } catch (e) { await write('failed', e); throw e; }
await write('completed');
return out;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Framework events recorded as code runs

Medium Severity

runEvent treats every non-compiled dispatchJob event as a triggerType: "code" run. Production ticks and webhooks arrive as FRIGG_TRIGGER_RUN and ON_WEBHOOK, not as NIGHTLY or WF_*, so those framework names are stored instead of the real handler, and a compiled schedule or webhook workflow is written twice. The tests only dispatchJob the inner event name, which the queue never does.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5d9f1be. Configure here.

Copy link
Copy Markdown
Contributor Author

Release review follow-up (folded in at 5d9f1be2):

Done

  • Status: ADR-063 is back to Proposed. The flip to Accepted is reverted.
  • ID_TAKEN: publishing an id another workspace owns now returns 404 NOT_FOUND, the same body as reading a foreign id (ADR-052 §3). Catalogue and code names still return 409.
    • Residual: a tenant can still infer an id exists by comparing that 404 with a 201 for a fresh id. Closing it needs ids scoped per owner.
  • Name clash: a definition shadowed by a code class now logs frigg.integrations.definition.shadowed_by_code (WARN, once per name).
  • Admin paths: final paths are /admin/integrations/..., with DEFINITION_ROUTES exported.
  • Triggers: published types are ticked through the new triggerTypes().

Waiting


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

prerelease This change is available in a prerelease. release Create a release when this pr is merged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants