[FEATURE] Dynamic workflows: ToolProvider as a first-class workflow step
Problem Statement
Dynamic workflows can already invoke tools as first-class graph steps ({ type: 'tool', toolId }), but that path only looks up tools registered on mastra.tools. Tool providers (Composio, Arcade, etc.) are a different catalog: Studio discovery plus per-request resolveToolsVNext with OAuth connections. Today they hydrate onto stored agents, not workflow graphs.
There is no persistable dynamic-workflow step that means “always call this provider tool with this connection.” Authors are stuck with:
- Pre-registering every integration on
mastra.tools(static; no per-user OAuth) - Custom
executeclosures (cannot persist as dynamic JSON) - An
{ type: 'agent' }step that may call the tool if the model chooses to
I’d like a deterministic, JSON-serializable step so a dynamic workflow can run a provider tool the same way it already runs a registered Mastra tool.
Proposed Solution
Add a new workflow graph leaf (and fluent builder method), not a workflow-level toolProviders bag and not an overload of { type: 'tool' }. Provider slugs (gmail.send_email) are not Mastra registry ids; stuffing them into toolId makes rehydrate throw “not registered” and collides ids.
Proposed serialized/live entry (name TBD; provider-tool suggested):
{
type: 'provider-tool';
id: string; // step result key
providerId: string; // e.g. 'composio' | 'arcade'
toolSlug: string; // e.g. 'gmail.send_email'
connection?: {
kind: 'author' | 'invoker' | 'platform' | 'caller-supplied';
connectionId?: string;
toolkit?: string;
};
options?: { retries?: number; metadata?: Record<string, unknown> };
}Fluent: workflow.providerTool({ providerId, toolSlug, connection }) next to .tool() / .agent().
Resolve at run, not at addDynamicWorkflow: editor.getToolProvider(providerId) → resolveToolsVNext({ toolSlugs, connection, requestContext }) → execute like runToolEntry (suspend, state, actor). Needed for invoker / caller-supplied OAuth.
Schema-flow: use provider tool JSON Schema for Studio / assertValidDynamicWorkflow where available; if schema is connection-dependent, validate structure + known provider/slug and enforce I/O at run.
This FR should include:
- New
SingleStepEntry/ serialized union member - Builder method + authoring schema
- Executor sibling of
runToolEntry; wire default + evented engines - Dynamic serialize / rehydrate / validate / graph walkers
- Docs (
agents-and-tools, dynamic workflow definition, workflow-methods reference) - Tests: round-trip, missing provider, connection kinds, execute with mocked provider
Follow-up (not blocking): Studio graph node that reuses the existing tool-provider picker.
Out of scope: MCP toolsets as a step; copying agent toolProviders snapshots onto workflows; replacing { type: 'tool' }.
Component
Workflows, Studio, Other (Editor ToolProvider)
Alternatives Considered
- Overload
{ type: 'tool', toolId }with provider slugs — registry vs provider ids collide; rehydrate viamastra.getToolfails - Workflow-level
toolProvidersmap like stored agents — workflows do not assemble an LLM tool bag; they run one step - Agent step only — model chooses tools; not deterministic
- Register every provider tool on
mastra.toolsat boot — loses per-user connections andcaller-suppliedresolve
Example Use Case
As a developer (or Studio/dynamic-workflow author), I add a step: Composio gmail.send_email with an invoker connection. A previous .map() supplies { to, subject, body }. The run always executes that provider tool — no LLM in the path — and the result is stepResults.sendEmail.
Additional Context
- Existing registered-tool step:
ToolStepEntryinpackages/core/src/workflows/types.ts({ type: 'tool', id, toolId }) - Execute path today:
packages/core/src/workflows/entry-executors/run-tool-entry.ts(entry.tool ?? mastra.getTool(entry.toolId)) - ToolProvider contract:
packages/core/src/tool-provider/types.ts(resolveTools/resolveToolsVNext, connection kinds) - Agent-only hydration:
packages/editor/src/namespaces/agent.tsviaresolveStoredToolProviders - Dynamic workflows persist JSON graphs with no closures:
packages/core/src/workflows/dynamic/ - Docs:
docs/src/content/en/docs/workflows/dynamic-workflows.mdx,docs/src/content/en/docs/workflows/agents-and-tools.mdx
Related accidental cursor[bot] noise from earlier (#22024, #22025–#22032) is unrelated; this is a separate product request.
Verification
- I have searched the existing issues to make sure this is not a duplicate
- I have provided sufficient context for the team to understand the request
Source: mastra-ai/mastra