Scrydon
ExtensionsAuthoring

Dynamic Tools (Per-connection Derived Tools)

Declare a discovery tool so the platform can provision connection-specific tools at runtime — custom objects, custom fields, custom record types — without rebuilding or republishing the extension code artifact.

Overview

Some platforms (CRM systems, ticketing tools, workflow engines) have schemas that depend on each customer's configuration. A discovery tool lets the platform ask the connected instance "what tools are available right now?" and provision a per-connection catalog of derived tools for the agent.

What doesn't change:

  • The extension code artifact is never rebuilt or re-uploaded for each customer.
  • Every derived tool runs inside the same extension_code Kata microVM as the static tool it wraps.
  • No new executing code is introduced. A derived tool is a narrower typed view of a statically shipped executor.

How it works

  1. You declare a dynamicTools capability in your manifest with a discoveryToolId reference — the slug of an existing static tool in the same toolkit.
  2. For an explicitly installed extension, the platform grants every exact executor id you declared, including delete executors. A manually configured extension still requires an explicit admin grant.
  3. The platform runs your discovery tool for one connection via the existing governed execution path.
  4. The platform applies a six-control admission pipeline to each descriptor your discovery tool returns.
  5. Admitted descriptors become connection-scoped tools in the agent catalog. They are stored in Postgres and refreshed after relevant configuration changes, when a stale Tools sub-sheet opens from Provides, or on a manual admin trigger.

Declaring dynamic tools in your manifest

// sdk-authoring: defineToolkit(...)
defineToolkit({
  id: "my-crm",
  name: "My CRM",
  // ... other toolkit fields ...

  dynamicTools: {
      discoveryToolId: "list-record-types", // slug of a static tool in this toolkit
      executors: [
        { toolId: "create-record", effect: "write" },
        { toolId: "update-record", effect: "write" },
        { toolId: "get-record",    effect: "read"  },
      ],
      refresh: { ttlSeconds: 3600 },        // optional staleness threshold
      maxTools: 50,                         // optional; server cap is always enforced
  },
});

discoveryToolId

Must be the slug of a tool already declared in the same toolkit. It must accept no required parameters (or accept only parameters the platform can supply from the active connection's profileConfig). This is the only tool the platform calls for discovery; the returned descriptors are never agent-selectable.

executors

An array of { toolId, effect } objects — the dispatch-target allowlist. A derived tool whose dispatch.tool is not in this array is withheld at admission. For an installed extension, every declared executor is automatically included in the connection's exact-id grant, including executors labelled delete. This is the trust delegated when an administrator installs a signed extension or explicitly accepts an unsigned extension's warning.

effect classifies the action for presentation and policy diagnostics. It does not grant authority: admission checks the executor's exact scoped tool id, not the claimed effect, so understating an effect gains nothing. Keep the declaration accurate and include only executors your extension genuinely needs.

extension code continues to use the authoring slug in both executors[].toolId and dispatch.tool. At installation, the platform resolves that slug to one unambiguous scoped executor identity before applying the exact-id admission check. A descriptor that supplies a different scoped id is never rewritten and is rejected.

Writing the discovery tool

Your discovery tool returns an array of DynamicToolDescriptor objects.

import type { DynamicToolDescriptor } from "@scrydon/sdk-authoring/extensions";

// Example: return one derived tool per custom object type
export async function listRecordTypes(ctx: ToolContext): Promise<DynamicToolDescriptor[]> {
  const objectTypes = await ctx.http.get("/api/v2/object_types");

  return objectTypes.map((obj) => ({
    key: obj.id,             // stable unique key for this descriptor
    name: obj.label,         // shown in the agent catalog (character-allowlisted)
    description: obj.description ?? `Create a ${obj.label} record`,
    input: {
      type: "object",
      properties: {
        values: { type: "object" },
      },
      required: ["values"],
    },
    dispatch: {
      tool: "create-record",  // must be in the executors array above
      bind: {
        objectType: obj.id,  // bound parameter value (not a credential or header)
      },
    },
  }));
}

Descriptor field rules

FieldRequiredNotes
keyYesStable unique string. Used for deterministic capacity truncation.
nameYesCharacter-allowlisted on admission. Keep it short and human-readable.
descriptionYesLength-capped and stripped of control characters on admission.
inputYesJSON Schema object for the parameters that remain visible after binding.
dispatch.toolYesExact slug of a static tool in the same toolkit. Must match an executor tool id in the admin's grant envelope.
dispatch.bindYesBound parameter values; use {} when none are needed. Must not contain credential references, HTTP headers, URL overrides, or connection selectors.

Admission controls and rejection

The platform runs every descriptor through six controls before admitting it:

  1. Structural validation — malformed descriptors are rejected.
  2. Executor allowlistdispatch.tool must be in the connection's exact-id grant. Descriptors outside the set are withheld. Installed-extension grants are reconciled from the server-resolved pinned code artifact, never from a browser-supplied executor list.
  3. Authority-binding refusal — any bind path that references a credential, HTTP header, URL, or connection selector is rejected and cannot be overridden by an admin.
  4. Descriptor neutralisation — names and descriptions are structurally cleaned.
  5. Capacity — total admitted tools are capped at min(grant.maxTools, MAX_DERIVED_TOOLS_PER_CONNECTION).
  6. Identity — a digest over the code artifact hash, executor tool id, descriptor content, and grant id is computed and stored.

withheld descriptors are visible to the admin, who may expand the envelope. rejected descriptors cannot be approved by any admin.

What the agent sees

Admitted derived tools appear in the agent catalog with:

  • id: the executor tool's exact scoped id (e.g. org:acme:my-crm:create-record)
  • name: your neutralised descriptor.name
  • description: your neutralised descriptor.description
  • Schema: derived from the executor tool, with bind values pre-filled

The agent cannot see the raw bind values or the grant id. Dispatch, credential binding, and policy resolution are byte-identical to the static executor.

Testing your discovery tool locally

Test it like any other tool — in isolation or with scrydon extension test:

scrydon extension test --tool list-record-types

For end-to-end testing of derived tool admission, use a staging connection and Provides → Tools on the extension settings page (described in the admin guide).

Verify at least these cases:

  1. A newly enabled connection discovers tools without a separate executor prompt.
  2. Create, update, and delete descriptors appear for the expected live objects.
  3. Two connections with different schemas show different catalogs.
  4. Sync now reflects added or removed objects.
  5. A failed refresh leaves the last successful catalog visible.

Limits

LimitValue
Max derived tools per connection500 (MAX_DERIVED_TOOLS_PER_CONNECTION); a grant may lower it
descriptor.name max length64 characters (DESCRIPTOR_NAME_MAX)
descriptor.description max length512 characters (DESCRIPTOR_DESCRIPTION_MAX)
dispatch.bind max keys20
dispatch.bind max value length1024 characters

FAQ

Can a derived tool call a different executor on different connections?

No. dispatch.tool is validated at admission time; it must be the same executor slug declared by the installed toolkit. A discovery result can use different declared executors for different descriptors (for example, create, update, and delete), but it cannot introduce a connection-selected or undeclared executor.

What happens when I update the executor tool's schema?

Derived tools inherit the executor's schema, including updates. The admitted set is refreshed on the next sync; descriptors that no longer pass admission are removed.

Can a publisher declare dynamicTools for a first-party Scrydon tool?

No. The first release supports organization-installed connections only.

After changing an installed extension’s code or a connection’s tool grant, refresh its discovered tools. Tools admitted under a previous artifact or grant remain unavailable until discovery admits them again. Duplicate descriptor keys are rejected together; other valid descriptors can still be admitted.

Operator-configured endpoints

For services such as Twenty whose API host depends on the connection, declare a non-secret endpoint explicitly:

configFields: [{
  key: "baseUrl",
  label: "Instance URL",
  required: true,
  type: "url",
  purpose: "endpoint",
}]

Extension code can construct URLs from ctx.profileConfig.baseUrl. The endpoint must use HTTPS on port 443, with no embedded credentials, query parameters, or fragment. Keep API keys in the connection credential, never in the URL or its path. Other config strings remain broker placeholders; type: "url" alone does not make a value visible to extension code.

An organization administrator must allow the endpoint domain in the organization egress allowlist. Each execution is bound to the exact configured hostname; subdomains and unrelated hosts are not implicitly granted. Vendor-host trust and platform default domains do not replace this approval. Public-address, DNS pinning, redirect, and metadata-address protections still apply.

On this page

On this page