#22043·mastra

[FEATURE] Dynamic workflows: ToolProvider as a first-class workflow step

Author: thomaslabergeCreated Aug 21, 2026Updated Sep 17, 2026
Labelsstatus: needs triagecustomer-eng

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 execute closures (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):

typescript
{
  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 via mastra.getTool fails
  • Workflow-level toolProviders map 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.tools at boot — loses per-user connections and caller-supplied resolve

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: ToolStepEntry in packages/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.ts via resolveStoredToolProviders
  • 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