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_codeKata 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
- You declare a
dynamicToolscapability in your manifest with adiscoveryToolIdreference — the slug of an existing static tool in the same toolkit. - 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.
- The platform runs your discovery tool for one connection via the existing governed execution path.
- The platform applies a six-control admission pipeline to each descriptor your discovery tool returns.
- 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
| Field | Required | Notes |
|---|---|---|
key | Yes | Stable unique string. Used for deterministic capacity truncation. |
name | Yes | Character-allowlisted on admission. Keep it short and human-readable. |
description | Yes | Length-capped and stripped of control characters on admission. |
input | Yes | JSON Schema object for the parameters that remain visible after binding. |
dispatch.tool | Yes | Exact slug of a static tool in the same toolkit. Must match an executor tool id in the admin's grant envelope. |
dispatch.bind | Yes | Bound 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:
- Structural validation — malformed descriptors are rejected.
- Executor allowlist —
dispatch.toolmust 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. - Authority-binding refusal — any
bindpath that references a credential, HTTP header, URL, or connection selector is rejected and cannot be overridden by an admin. - Descriptor neutralisation — names and descriptions are structurally cleaned.
- Capacity — total admitted tools are capped at
min(grant.maxTools, MAX_DERIVED_TOOLS_PER_CONNECTION). - 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 neutraliseddescriptor.namedescription: your neutraliseddescriptor.description- Schema: derived from the executor tool, with
bindvalues 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-typesFor 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:
- A newly enabled connection discovers tools without a separate executor prompt.
- Create, update, and delete descriptors appear for the expected live objects.
- Two connections with different schemas show different catalogs.
- Sync now reflects added or removed objects.
- A failed refresh leaves the last successful catalog visible.
Limits
| Limit | Value |
|---|---|
| Max derived tools per connection | 500 (MAX_DERIVED_TOOLS_PER_CONNECTION); a grant may lower it |
descriptor.name max length | 64 characters (DESCRIPTOR_NAME_MAX) |
descriptor.description max length | 512 characters (DESCRIPTOR_DESCRIPTION_MAX) |
dispatch.bind max keys | 20 |
dispatch.bind max value length | 1024 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.