Build an Automation With the AI Assistant
Use this guide when you want the fastest path from human intent to a real automationsV2 payload.
It is designed for two audiences at once:
- humans in the control panel who want a guided builder
- agents that need to author the same structure from prompts or SDK code
What this flow proves
The AI-first composer shows that Tandem can do all of the following without changing engine semantics:
- turn a natural-language prompt into a governed plan
- ask a clarification question when the goal is ambiguous
- emit an explicit
automationsV2.createpayload - preview the resulting JSON or YAML before creation
- create the automation and run it immediately
- keep run history visible in the standard automation views
The control-panel flow
Open Automations and select the AI Composer tab if it is enabled for the workspace.
Then follow this rhythm:
- describe the goal in plain English
- answer any clarifying question
- review the generated runbook replay
- inspect the JSON or YAML payload
- create the automation
- run it now if you want to verify the first pass immediately
The composer is intentionally prompt-first. It should feel like talking to a strong operator who can also surface structure.
Agent Creation Test Path
Use the composer test controls to validate agent governance behavior before shipping a real external creator.
- switch on Testing mode
- set a synthetic
agent id - create and run the automation
In this mode the request headers are sent as:
x-tandem-agent-test-mode: 1x-tandem-request-source: agentx-tandem-agent-id: <your synthetic agent id>
If governance blocks the request, the response includes the same AUTOMATION_V2_AGENT_* and
AUTOMATION_V2_CAPABILITY_ESCALATION_* codes you would receive from a non-UI agent client.
What a good prompt looks like
Build a governed automation named "Todo digest + notify" for /workspace/repos/my-repo.Use a file-reading step to find TODO and FIXME items under src/ and docs/.Write docs/todo_digest.md with path, line number, and severity.End with an MCP step that sends a short Slack summary and includes the report path.Keep the schedule manual for the first pass.If Tandem needs more detail, it should ask a narrow clarification question instead of guessing.
What Tandem generates
The generated structure should be easy to inspect and reason about:
nameandstatusschedulewith a manual, cron, or interval policyworkspace_rootagentswith per-agent tool and MCP policyflow.nodeswith explicit dependenciesmetadata.composerwith provenance for later handoff/debugging
That means you can safely preview, validate, and recreate the same payload from SDK code later.
If the goal is recurring, the same payload should include a schedule field before it is created. The composer should not treat recurrence as a separate afterthought.
From prompt to SDK call
The important bridge is not just that Tandem can draft a payload. It is that the same payload can be passed directly into the engine.
For raw automation JSON, the equivalent SDK call is:
const created = await client.automationsV2.create(generatedPayload as any);await client.automationsV2.runNow(created.automation?.automation_id ?? created.automation_id);created = await client.automations_v2.create(generated_payload)await client.automations_v2.run_now(created.automation_id or "")For planner bundle JSON, use the workflow-plan import surface instead:
const preview = await client.workflowPlans.importPreview({ bundle: planBundle });await client.workflowPlans.importPlan({ bundle: preview.bundle ?? planBundle });That distinction matters:
- use
automationsV2.createfor a full automation object - use
workflowPlans.importPreview/workflowPlans.importPlanfor a workflow-plan bundle - use
workflowPlans.chatStart/chatMessage/applyonly when the agent is still shaping the plan in conversation
Simple example: digest + notify
This is the most direct proof of value: read files, write a report, then notify through MCP.
const created = await client.automationsV2.create({ name: "Todo digest + notify", status: "active", schedule: { type: "manual", timezone: "UTC", misfire_policy: { type: "run_once" }, }, workspace_root: "/workspace/repos/my-repo", agents: [ { agent_id: "reader", display_name: "Reader", tool_policy: { allowlist: ["read", "write"] }, mcp_policy: { allowed_servers: [], allowed_tools: [] }, approval_policy: "auto", }, { agent_id: "notifier", display_name: "Notifier", tool_policy: { allowlist: ["read"] }, mcp_policy: { allowed_servers: ["slack"], allowed_tools: ["send_message"] }, approval_policy: "auto", }, ], flow: { nodes: [ { node_id: "collect_todos", agent_id: "reader", objective: "Find TODO and FIXME items under src/ and docs/ with file + line context.", }, { node_id: "write_report", agent_id: "reader", depends_on: ["collect_todos"], objective: "Create docs/todo_digest.md with grouped findings and severity ranking.", }, { node_id: "notify_team", agent_id: "notifier", depends_on: ["write_report"], objective: "Use MCP to send a short summary to team and include path docs/todo_digest.md.", }, ], }, creator_id: "demo-operator",});
await client.automationsV2.runNow(created.automation?.automation_id);Complex example: file scan -> report -> MCP finish
This is the pattern that tends to impress both operators and agent developers:
- read local files first
- produce a durable artifact
- end with an external action through MCP
complex_automation = await client.automations_v2.create({ "name": "Repo risk radar", "status": "active", "schedule": { "type": "interval", "interval_seconds": 12 * 60 * 60, "timezone": "UTC", "misfire_policy": {"type": "run_once"}, }, "workspace_root": "/workspace/repos/my-repo", "agents": [ { "agent_id": "scanner", "display_name": "Scanner", "tool_policy": {"allowlist": ["read"]}, "mcp_policy": {"allowed_servers": [], "allowed_tools": []}, "approval_policy": "auto", }, { "agent_id": "analyst", "display_name": "Analyst", "tool_policy": {"allowlist": ["read", "write"]}, "mcp_policy": {"allowed_servers": [], "allowed_tools": []}, "approval_policy": "auto", }, { "agent_id": "notifier", "display_name": "Notifier", "tool_policy": {"allowlist": ["read"]}, "mcp_policy": {"allowed_servers": ["slack"], "allowed_tools": ["send_message"]}, "approval_policy": "auto", }, ], "flow": { "nodes": [ { "node_id": "scan_sources", "agent_id": "scanner", "objective": "Find TODO/FIXME patterns in src/, docs/, and README files.", }, { "node_id": "build_risk_report", "agent_id": "analyst", "depends_on": ["scan_sources"], "objective": "Create docs/todo_digest.md with risk tiers, rationale, and exact file references.", }, { "node_id": "notify_and_link", "agent_id": "notifier", "depends_on": ["build_risk_report"], "objective": "Send a short Slack summary and include docs/todo_digest.md as the handoff path.", }, ] },})
await client.automations_v2.run_now(complex_automation.automation_id)Clarification behavior
If the goal is ambiguous, the composer should ask one focused question.
When the question has obvious choices, the UI can render buttons. When it does not, the user can answer in free text.
This is the same behavior agents should emulate when they are generating workflows from prompts:
- prefer a small clarification over a risky assumption
- preserve the chosen branch in the plan conversation
- keep the final payload deterministic once the question is answered
SDK handoff
If you already know the shape you want, you can skip the conversation and create the payload directly from code.
If you want the conversational starting point plus the code path side by side, use the examples page first and then come back here.