Tracecat Automation Best Practices
For Slack-facing automations, use $tracecat-slackbot-best-practices. In Tracecat Workspace Chat,
load $tracecat-workspace-chat first; its host-specific tool mapping overrides the external MCP
steps below. On-demand references: graph-shape,
expressions, run-python,
tables, trigger-inputs,
case-triggers, secrets,
agent-outputs, agent-presets,
workflow-editing.
Workflow
For an external Tracecat MCP connection, start from live context rather than guessing:
- Discover the workspace with
list_workspaces.
- Read
tracecat://platform/dsl-reference when DSL syntax or examples are needed.
- Use
get_workflow_authoring_context for action schemas, variables, and secrets.
- For existing workflows, use
get_workflow, then targeted edit_workflow patches — see
workflow-editing.
- Validate with
validate_workflow, run a draft or published execution when appropriate,
then inspect failures with list_workflow_executions and get_workflow_execution.
Other hosts can expose a different tool surface. Follow their adapter rather than mechanically
prefixing these bare MCP names.
Clarify production choices that change the workflow contract: workspace, integration/provider,
secret source, publish/run behavior, destructive side effects, approvals, or acceptance
criteria. If the request is already specific enough, proceed.
Graph shape
Sketch the shape before authoring and keep it as close to one line as the work allows. Prefer
linear over parallel, and readable over fully connected: fewer edges is the metric.
depends_on is execution order, not data wiring. An action can read
ACTIONS.<ref>.result from any ancestor, not only a direct parent — validation checks that
the ref exists in the workflow, not that it is a parent. Add an edge when the action must
wait, never to make a value reachable.
- Default to a single chain. Branch only for genuinely independent work worth running
concurrently, and rejoin only when a later action needs results from more than one branch.
- Do not re-assert an upstream condition in
run_if. A skip propagates to every task whose
dependency edges are all skipped, so one gate covers everything below it on a chain. The
exception is a join, and it is a trap: a task with several parents is not force-skipped
while one survives, but the default join_strategy: all then requires every parent to
have been visited, so one skipped branch makes the join unreachable and fails the
workflow. Use join_strategy: any, or repeat the branch's condition on the join so it
self-skips first.
- One parent is the norm. More than one means a deliberate join — choose
join_strategy on
purpose.
- Keep ordinary workflows around 20 nodes or fewer and agentic workflows around 6 nodes or
fewer. Prefer readable left-to-right or top-to-bottom flows, human-readable refs, and layout
refs aligned with definition refs.
See graph-shape for a worked over-connected rewrite, join
strategies, error edges, and why scatter is not branching.
Choosing between an agent and Python
Decide by the kind of work, and optimize the whole workflow for maintainability and
readability — the fewest actions and the least code that does the job.
- Agentic work → use an agent (
ai.agent / ai.preset_agent). Anything needing
judgment, investigation, routing, enrichment choices, summarization, composing a message,
or posting to Slack. Give the agent the tools and trust it. Prefer agents, prompts, and
skills — they carry the creative thinking with far fewer moving parts than a graph of
deterministic nodes.
- Deterministic data plumbing → use Python (
core.script.run_python). Transforming,
normalizing, redacting, loading or upserting rows into tables, forwarding data between
systems. This is exactly what Python is for — it is not a smell. One clear run_python
action beats scattering the same work across many nodes.
The smell is using Python for the agentic part — for example composing or sending an
agent's Slack message from a script instead of giving the agent the Slack tool.
Prompt-size guardrail: push behavior into the prompt up to a reasonable size. When a
single prompt grows too large to stay readable, decompose into subagents or a skill
rather than inflating one mega-prompt.
Agent-First Automation
When the user asks for an agentic workflow, an agent preset, or says to use agents, make the
agent the primary owner of the work. Give it the tools it needs and trust it to investigate,
decide, route, compose messages, and act. Default to a thin shape:
trigger -> reshape/redact -> upsert event record -> ai.agent (or ai.preset_agent).
Persist the normalized event before the agent runs. When events originate in a SIEM, log
platform, SaaS webhook stream, audit trail, or cloud service, store the normalized payload
and review state in a Tracecat table first, with a deterministic upsert. That gives retries,
duplicate webhooks, and replay a durable source of truth. Point the agent at the saved row,
so the workflow — not the agent — owns the first durable record of the event.
A deterministic node earns its place by being thin, direct, and easy to audit, and by doing
work the agent should not own: redact secrets before the agent sees data, normalize schema,
upsert the event row, enforce hard approval or authorization boundaries, guard expensive
agent runs with a dedupe check, prepare bounded batches, or checkpoint durable state. Keep
judgment, routing, enrichment, message composition, soft approval decisions, and final
notification behavior in the agent. When an approval question itself needs judgment, let the
agent decide or draft the request, and reserve deterministic gates for explicit safety or
authorization boundaries.
Favor one well-instructed agent or reusable preset over many small deterministic nodes. Put
output contracts, Slack style, dedupe rules, tenant boundaries, tool permissions, and
permitted side effects directly in the preset instructions — the agent reads those, not repo
files. When the agent needs a tool, grant it the narrow tool directly rather than wrapping
the same call in a workflow action.
A preset's runtime has more capability than list_actions shows: a real shell and file tools,
Python, curl, jq, and DuckDB. Inspect the preset/runtime context before adding a workflow
helper to compensate for a presumed limitation — see
agent-presets.
Authoring Defaults
- For agentic workflows, push most behavior into the agent prompt/preset. For event-driven
workflows, a single reshape/redact/upsert action before the agent is usually the right
deterministic boundary.
- Use
ai.agent / ai.preset_agent for the agentic work; core.http_request for
deterministic API calls; core.script.run_python for deterministic data work, once an inline
expression, run_if, or a core.transform.* action has been ruled out — see
expressions and
run-python.
- For bulk table writes, prefer one native
core.table.insert_rows action when rows are already
shaped (up to 1000 rows per batch). If shaping, chunking, or mixed table/case writes are needed,
use core.script.run_python with imported helpers and bounded batches instead of scattering many
DB-backed actions.
- Do not give
core.http_request to an agent unless the user explicitly accepts the broad
network capability. Put deterministic HTTP in the workflow graph or a tightly scoped subflow.
- Split into subflows only when there is a real orchestration boundary, reusable child
workflow, separate execution history, approvals, long-running actions, or independent
checkpointing. Use
core.workflow.execute and prefer workflow_alias over hard-coded IDs.
Subflow bulk defaults: loop_strategy: batch, batch_size: 32, fail_strategy: isolated,
wait_strategy: wait (use detach only when the parent does not need child results).
- Keep run-python and agent outputs small: downstream rows, summary counts, bounded error
samples.
- Prefer a reusable agent preset over inline
ai.agent, and the model object over the
deprecated top-level model_name/model_provider — see
agent-presets.
Common Mistakes
- Using Python for the agentic part — composing or posting an agent's Slack message, or
making routing/judgment calls in a script. The agent owns message composition, posting (via
a Slack tool), and judgment; Python owns deterministic data plumbing.
- Over-connecting the graph: an edge from every producer to every consumer, plus a
run_if on
every branch repeating a condition an ancestor already enforced. Read from ancestors, keep
one chain, and gate once (see graph-shape).
- Workflow action names are not MCP tool names. Use MCP tools such as
create_workflow to
manage workflows, and action names such as core.http_request only inside workflow YAML.
- Inventing
tools.* action names. Discover actions with list_actions, then inspect exact
schemas with get_action_context.
- Using
for_each for ordinary loop management, or using scatter/gather for data-heavy
batching, filtering, joins, or table writes. Default to scatter for workflow-level loops;
add gather only when downstream steps need combined results, and omit it otherwise. Be especially
careful with >10 scattered DB-backed table/case actions, which can exhaust Postgres connection
slots. Scatter's optional interval can stagger work, but it does not batch DB writes; use one
core.table.insert_rows action or core.script.run_python batching for data-heavy processing
(see run-python).
- Using
insert_rows(..., upsert=True) without a unique index on the key column (see
tables).
- Granting
core.http_request to an agent without explicit user approval of broad network
access.
- Defining an agent
output_type nothing downstream branches on. Default to none, give the
agent the tool, and let the side effect be the output; ask the user first (see
agent-outputs).
1---2name: tracecat-automation-best-practices3description: Use when building, editing, validating, or debugging generic Tracecat automations through Tracecat MCP, including workflow DSL/YAML authoring, table design, unique indexes, run-python Tracecat imports, agent presets, ai.agent or ai.preset_agent workflows, executions, validation, and workflow best practices.4---56# Tracecat Automation Best Practices78For Slack-facing automations, use `$tracecat-slackbot-best-practices`. In Tracecat Workspace Chat,9load `$tracecat-workspace-chat` first; its host-specific tool mapping overrides the external MCP10steps below. On-demand references: [graph-shape](references/graph-shape.md),11[expressions](references/expressions-and-conditions.md), [run-python](references/run-python.md),12[tables](references/tables.md), [trigger-inputs](references/trigger-inputs.md),13[case-triggers](references/cases-and-triggers.md), [secrets](references/secrets-and-oauth.md),14[agent-outputs](references/agent-outputs.md), [agent-presets](references/agent-presets.md),15[workflow-editing](references/workflow-editing.md).1617## Workflow1819For an external Tracecat MCP connection, start from live context rather than guessing:20211. Discover the workspace with `list_workspaces`.222. Read `tracecat://platform/dsl-reference` when DSL syntax or examples are needed.233. Use `get_workflow_authoring_context` for action schemas, variables, and secrets.244. For existing workflows, use `get_workflow`, then targeted `edit_workflow` patches — see25 [workflow-editing](references/workflow-editing.md).265. Validate with `validate_workflow`, run a draft or published execution when appropriate,27 then inspect failures with `list_workflow_executions` and `get_workflow_execution`.2829Other hosts can expose a different tool surface. Follow their adapter rather than mechanically30prefixing these bare MCP names.3132Clarify production choices that change the workflow contract: workspace, integration/provider,33secret source, publish/run behavior, destructive side effects, approvals, or acceptance34criteria. If the request is already specific enough, proceed.3536## Graph shape3738Sketch the shape before authoring and keep it as close to one line as the work allows. Prefer39linear over parallel, and readable over fully connected: **fewer edges is the metric.**4041- **`depends_on` is execution order, not data wiring.** An action can read42 `ACTIONS.<ref>.result` from any ancestor, not only a direct parent — validation checks that43 the ref exists in the workflow, not that it is a parent. Add an edge when the action must44 *wait*, never to make a value reachable.45- Default to a single chain. Branch only for genuinely independent work worth running46 concurrently, and rejoin only when a later action needs results from more than one branch.47- **Do not re-assert an upstream condition in `run_if`.** A skip propagates to every task whose48 dependency edges are all skipped, so one gate covers everything below it on a chain. The49 exception is a join, and it is a trap: a task with several parents is not force-skipped50 while one survives, but the default `join_strategy: all` then requires *every* parent to51 have been visited, so one skipped branch makes the join unreachable and **fails the52 workflow**. Use `join_strategy: any`, or repeat the branch's condition on the join so it53 self-skips first.54- One parent is the norm. More than one means a deliberate join — choose `join_strategy` on55 purpose.56- Keep ordinary workflows around 20 nodes or fewer and agentic workflows around 6 nodes or57 fewer. Prefer readable left-to-right or top-to-bottom flows, human-readable refs, and layout58 refs aligned with definition refs.5960See [graph-shape](references/graph-shape.md) for a worked over-connected rewrite, join61strategies, error edges, and why scatter is not branching.6263## Choosing between an agent and Python6465Decide by the *kind* of work, and optimize the whole workflow for maintainability and66readability — **the fewest actions and the least code that does the job.**6768- **Agentic work → use an agent** (`ai.agent` / `ai.preset_agent`). Anything needing69 judgment, investigation, routing, enrichment choices, summarization, composing a message,70 or posting to Slack. Give the agent the tools and trust it. Prefer agents, prompts, and71 skills — they carry the creative thinking with far fewer moving parts than a graph of72 deterministic nodes.73- **Deterministic data plumbing → use Python** (`core.script.run_python`). Transforming,74 normalizing, redacting, loading or upserting rows into tables, forwarding data between75 systems. This is exactly what Python is for — it is **not** a smell. One clear `run_python`76 action beats scattering the same work across many nodes.7778The smell is using Python for the *agentic* part — for example composing or sending an79agent's Slack message from a script instead of giving the agent the Slack tool.8081**Prompt-size guardrail:** push behavior into the prompt up to a reasonable size. When a82single prompt grows too large to stay readable, decompose into **subagents or a skill**83rather than inflating one mega-prompt.8485## Agent-First Automation8687When the user asks for an agentic workflow, an agent preset, or says to use agents, make the88agent the primary owner of the work. Give it the tools it needs and trust it to investigate,89decide, route, compose messages, and act. Default to a thin shape:90`trigger -> reshape/redact -> upsert event record -> ai.agent` (or `ai.preset_agent`).9192Persist the normalized event before the agent runs. When events originate in a SIEM, log93platform, SaaS webhook stream, audit trail, or cloud service, store the normalized payload94and review state in a Tracecat table first, with a deterministic upsert. That gives retries,95duplicate webhooks, and replay a durable source of truth. Point the agent at the saved row,96so the workflow — not the agent — owns the first durable record of the event.9798A deterministic node earns its place by being thin, direct, and easy to audit, and by doing99work the agent should not own: redact secrets before the agent sees data, normalize schema,100upsert the event row, enforce hard approval or authorization boundaries, guard expensive101agent runs with a dedupe check, prepare bounded batches, or checkpoint durable state. Keep102judgment, routing, enrichment, message composition, soft approval decisions, and final103notification behavior in the agent. When an approval question itself needs judgment, let the104agent decide or draft the request, and reserve deterministic gates for explicit safety or105authorization boundaries.106107Favor one well-instructed agent or reusable preset over many small deterministic nodes. Put108output contracts, Slack style, dedupe rules, tenant boundaries, tool permissions, and109permitted side effects directly in the preset instructions — the agent reads those, not repo110files. When the agent needs a tool, grant it the narrow tool directly rather than wrapping111the same call in a workflow action.112113A preset's runtime has more capability than `list_actions` shows: a real shell and file tools,114Python, `curl`, `jq`, and DuckDB. Inspect the preset/runtime context before adding a workflow115helper to compensate for a presumed limitation — see116[agent-presets](references/agent-presets.md).117118## Authoring Defaults119120- For agentic workflows, push most behavior into the agent prompt/preset. For event-driven121 workflows, a single reshape/redact/upsert action before the agent is usually the right122 deterministic boundary.123- Use `ai.agent` / `ai.preset_agent` for the agentic work; `core.http_request` for124 deterministic API calls; `core.script.run_python` for deterministic data work, once an inline125 expression, `run_if`, or a `core.transform.*` action has been ruled out — see126 [expressions](references/expressions-and-conditions.md) and127 [run-python](references/run-python.md).128- For bulk table writes, prefer one native `core.table.insert_rows` action when rows are already129 shaped (up to 1000 rows per batch). If shaping, chunking, or mixed table/case writes are needed,130 use `core.script.run_python` with imported helpers and bounded batches instead of scattering many131 DB-backed actions.132- Do not give `core.http_request` to an agent unless the user explicitly accepts the broad133 network capability. Put deterministic HTTP in the workflow graph or a tightly scoped subflow.134- Split into subflows only when there is a real orchestration boundary, reusable child135 workflow, separate execution history, approvals, long-running actions, or independent136 checkpointing. Use `core.workflow.execute` and prefer `workflow_alias` over hard-coded IDs.137 Subflow bulk defaults: `loop_strategy: batch`, `batch_size: 32`, `fail_strategy: isolated`,138 `wait_strategy: wait` (use `detach` only when the parent does not need child results).139- Keep run-python and agent outputs small: downstream rows, summary counts, bounded error140 samples.141- Prefer a reusable agent preset over inline `ai.agent`, and the `model` object over the142 deprecated top-level `model_name`/`model_provider` — see143 [agent-presets](references/agent-presets.md).144145## Common Mistakes146147- Using Python for the *agentic* part — composing or posting an agent's Slack message, or148 making routing/judgment calls in a script. The agent owns message composition, posting (via149 a Slack tool), and judgment; Python owns deterministic data plumbing.150- Over-connecting the graph: an edge from every producer to every consumer, plus a `run_if` on151 every branch repeating a condition an ancestor already enforced. Read from ancestors, keep152 one chain, and gate once (see [graph-shape](references/graph-shape.md)).153- Workflow action names are not MCP tool names. Use MCP tools such as `create_workflow` to154 manage workflows, and action names such as `core.http_request` only inside workflow YAML.155- Inventing `tools.*` action names. Discover actions with `list_actions`, then inspect exact156 schemas with `get_action_context`.157- Using `for_each` for ordinary loop management, or using scatter/gather for data-heavy158 batching, filtering, joins, or table writes. Default to scatter for workflow-level loops;159 add gather only when downstream steps need combined results, and omit it otherwise. Be especially160 careful with >10 scattered DB-backed table/case actions, which can exhaust Postgres connection161 slots. Scatter's optional interval can stagger work, but it does not batch DB writes; use one162 `core.table.insert_rows` action or `core.script.run_python` batching for data-heavy processing163 (see [run-python](references/run-python.md)).164- Using `insert_rows(..., upsert=True)` without a unique index on the key column (see165 [tables](references/tables.md)).166- Granting `core.http_request` to an agent without explicit user approval of broad network167 access.168- Defining an agent `output_type` nothing downstream branches on. Default to none, give the169 agent the tool, and let the side effect be the output; ask the user first (see170 [agent-outputs](references/agent-outputs.md)).