Skip to content

Build your first workflow

This is a build-it-by-hand tutorial. We’ll make a small but real workflow: it takes a topic, has a model write a short executive brief on it, runs a little Python to tidy the result and count a few things, and returns the lot. Every screenshot below is the real editor doing exactly this. Once you’ve done it once, every other workflow is just more of the same.

The finished workflow on the canvas: an Input node named 'input' wired to an LLM node 'llm', wired to a Python node 'python', wired to an Output node 'output', with the node palette grouped by Flow, AI, Code, Actions and Integration on the right.

Open Workflows from the sidebar and click New Workflow. You’ll see three regions:

  • The canvas — the middle, where your nodes live and connect.
  • The palette (right) — nodes grouped by what they do: Flow (Input, Output, Condition, Loop), AI (LLM, Agent), Code (Python, Template), Actions (Email, Image, Search), and Integration (Tool, HTTP, Sub-workflow).
  • The toolbar (top) — Save, Run, History, Files, Schedule, JSON, and Edit with AI. The workflow’s name sits at the far left — click it to rename.

You add a node by dragging it from the palette onto the canvas.

  1. Name it. Click the title (it starts as Untitled Workflow) and call it something like Executive brief from a topic.

  2. Add an Input node. Drag Input onto the canvas, click it to open the Properties panel on the right, and add a parameter called topic of type string. This is the value you’ll supply each run.

    The Input node selected, with the Properties panel showing a single parameter named 'topic' of type string and the description 'What to write the brief about'.
  3. Add an LLM node. Drag LLM on. In Properties, pick a Model (here, claude-opus-4-8) and write the prompt on the Prompt tab. Refer to the input by name:

    Write a tight 5-bullet executive brief on {{ input.parameters.topic }}.
    One bullet per line. Start each line with "- ". No preamble, no closing line.
    The LLM node selected, Properties showing Model set to claude-opus-4-8 and a Prompt tab containing the brief prompt that references input.parameters.topic.

    That {{ input.parameters.topic }} reference is how you wire nodes together — Catalyst sees it and knows the Input node has to run first. (More on references below.)

  4. Add a Python node. Drag Python on and paste this into its code field. The LLM node’s answer arrives as inputs["text"]; this turns the bullets into a numbered list and counts a couple of things:

    # The LLM node's output arrives over the edge as inputs["text"].
    # Turn the bullet list into a numbered list and count a few things.
    brief = inputs.get("text", "")
    bullets = [line.lstrip("- ").strip() for line in brief.splitlines() if line.strip()]
    result = {
    "bullet_count": len(bullets),
    "word_count": len(brief.split()),
    "numbered": "\n".join(f"{i + 1}. {b}" for i, b in enumerate(bullets)),
    }
    The Python node's code editor maximised, showing the code that reads inputs['text'], splits it into bullets, and builds a result dict with bullet_count, word_count and a numbered string.

    Two rules for Python nodes: read your inputs from the inputs dict, and assign your answer to a variable called result. Whatever result holds is what the node outputs.

  5. Add an Output node. Drag Output on. In Properties, map the fields you want the run to return to the upstream values that produce them:

    • brief{{ llm.text }}
    • numbered_brief{{ python.result.numbered }}
    • bullet_count{{ python.result.bullet_count }}
    • word_count{{ python.result.word_count }}
    The Output node selected, Properties showing a Mappings list: brief mapped to llm.text, plus word_count, bullet_count and numbered mapped to python.result fields.
  6. Connect the nodes. Drag from the dot on the right edge of one node to the dot on the left edge of the next, in order: input → llm → python → output. The lines you draw set the run order; the {{ … }} references decide what data flows where.

  1. Click Run. Because the workflow declares a topic parameter, Catalyst asks for it first.

    The Workflow Parameters dialog asking for a single string parameter, 'topic', with an Enter topic field and a Run button.
  2. Type a topic and click Run. The runner opens and each node lights up as it executes — green when it’s done. When everything’s finished you’ll see Completed and 4 / 4 nodes.

    The run dialog showing status Completed and 4 of 4 nodes, with rows for input, llm (showing input/output token counts), python and output, each with a green check.
  3. Click any node row to see what it produced. The output node shows the final result — the brief, the numbered version, and the counts.

    The output node row expanded, showing the returned JSON: a 'brief' string of executive bullets plus the numbered brief and counts.

Here’s the whole run, start to finish:

Click Save and the workflow joins your Workflows list, with its node and connection counts — ready to run again, schedule, or share.

The Workflows list showing the saved 'Executive brief from a topic' workflow (4 nodes · 3 connections) alongside other workflows.

A second example: crunch a CSV with Python

Section titled “A second example: crunch a CSV with Python”

Not every workflow needs a model. Sometimes you just want to run code over a file you bring. You can attach a file to the workflow and read it from a Python node — the file mounts read-only at inputs/<name> inside the sandbox on every run. Here’s a tiny one: Input → Python (pandas) → Output over a sales.csv.

  1. Attach the file. Click Files in the toolbar, then Attach file, and pick your CSV. It’s now bundled with the workflow — every run sees it at inputs/sales.csv.

    The Workflow Attachments dialog with sales.csv (256 B, text/csv) attached, and an explanation that files mount read-only at ./inputs/<name> inside the python sandbox.
  2. Read it in Python. Drag a Python node on and read the file with pandas — here we coerce a messy column, then total revenue by region:

    import pandas as pd
    # Files attached to the workflow are mounted read-only at inputs/.
    df = pd.read_csv("inputs/sales.csv")
    # Coerce a messy column and total revenue by region.
    df["units"] = pd.to_numeric(df["units"], errors="coerce").fillna(0).astype(int)
    by_region = df.groupby("region")["revenue"].sum().sort_values(ascending=False)
    result = {
    "rows": int(len(df)),
    "total_revenue": int(df["revenue"].sum()),
    "top_region": by_region.index[0],
    "by_region": {r: int(v) for r, v in by_region.items()},
    }
    The Python node's code editor showing pandas reading inputs/sales.csv, coercing the units column, grouping revenue by region, and assigning a result dict.

    The sandbox ships with pandas, numpy, matplotlib and other common libraries, so there’s nothing to install.

  3. Map the outputs and run. Point an Output node at {{ python.result.rows }}, {{ python.result.total_revenue }}, {{ python.result.top_region }}, and {{ python.result.by_region }}. This workflow has no parameters, so Run executes straight away — it reads the attached CSV and returns the summary.

    The run dialog Completed at 3 of 3 nodes, the output node expanded showing rows 12, total_revenue 259400, top_region North, and revenue by region.

The one idea that unlocks everything: a node reads another node’s output by name, written as {{ nodename.field }}.

  • {{ input.parameters.topic }} — the topic value you supplied to the input node.
  • {{ llm.text }} — the llm node’s generated text.
  • {{ python.result.numbered }} — the numbered key inside the python node’s result.

Each node type publishes its own fields — an LLM node has text, a Python node has result, an Input node has parameters. If you mistype a name, the run stops and tells you exactly which reference was wrong — better a clear error than a silent wrong answer.