""" Creates n8n workflow definitions for every entity in the experiments database. Each workflow has an Execute Workflow Trigger + Switch (routes by `action` field) + one Postgres node per operation. They act as reusable sub-workflows. """ import json, uuid, requests API = "http://localhost:5678/api/v1" KEY = "n8n_api_ca07bd874c628ed0f52d9a2e65073960f9f617c5" CRED = {"id": "9iJuyA9iR5KUzmj5", "name": "Experiments DB (PostgreSQL)"} HEADS = {"X-N8N-API-KEY": KEY, "Content-Type": "application/json"} def uid(): return str(uuid.uuid4()) # ── Node builders ────────────────────────────────────────────────────────────── def trigger_node(x, y): return { "id": uid(), "name": "Trigger", "type": "n8n-nodes-base.executeWorkflowTrigger", "typeVersion": 1.1, "position": [x, y], "parameters": {}, } def switch_node(name, actions, x, y): """Route by $json.action — one output per action string.""" rules = [] for action in actions: rules.append({ "conditions": { "options": {"caseSensitive": False, "leftValue": "", "typeValidation": "loose"}, "combinator": "and", "conditions": [{ "leftValue": "={{ $json.action }}", "rightValue": action, "operator": {"type": "string", "operation": "equals"}, }], }, "renameOutput": True, "outputKey": action, }) return { "id": uid(), "name": name, "type": "n8n-nodes-base.switch", "typeVersion": 3.2, "position": [x, y], "parameters": {"rules": {"values": rules}, "options": {}}, } def pg_node(name, query, params, x, y): """Postgres executeQuery node. params is a list of n8n expressions for $1,$2,...""" node = { "id": uid(), "name": name, "type": "n8n-nodes-base.postgres", "typeVersion": 2.5, "position": [x, y], "credentials": {"postgres": CRED}, "parameters": { "operation": "executeQuery", "query": query, "options": {}, }, } if params: node["parameters"]["options"]["queryReplacement"] = "={{ [" + ", ".join(params) + "] }}" return node # ── Connection helpers ────────────────────────────────────────────────────────── def conn(src, src_idx, dst): """Return a connections dict fragment.""" return (src["name"], src_idx, dst["name"]) def build_connections(pairs): """ pairs: list of (src_node, output_index, dst_node) Returns n8n connections object. """ conns = {} for src, idx, dst in pairs: conns.setdefault(src, {"main": []}) while len(conns[src]["main"]) <= idx: conns[src]["main"].append([]) conns[src]["main"][idx].append({"node": dst, "type": "main", "index": 0}) return conns # ── Workflow factory ─────────────────────────────────────────────────────────── def make_workflow(name, trigger, switch, pg_nodes, connections): return { "name": name, "nodes": [trigger, switch] + pg_nodes, "connections": connections, "settings": {"executionOrder": "v1"}, } def post_workflow(wf): r = requests.post(f"{API}/workflows", headers=HEADS, json=wf) if r.ok: d = r.json() print(f" ✓ [{d['id']}] {wf['name']}") else: print(f" ✗ {wf['name']} → {r.status_code} {r.text[:200]}") # ── Layouts ──────────────────────────────────────────────────────────────────── # Nodes stacked vertically, 220px apart; Switch at col 300, pg nodes at col 560 def pg_y(i): return i * 220 - 440 # center the stack # ══════════════════════════════════════════════════════════════════════════════ # 1. EXPERIMENTS # ══════════════════════════════════════════════════════════════════════════════ def wf_experiments(): t = trigger_node(60, 0) sw = switch_node("Route", ["list","get","create","update","delete"], 300, 0) ops = [ pg_node("List Experiments", "SELECT id, title, created_at FROM experiments ORDER BY created_at DESC", [], 560, pg_y(0)), pg_node("Get Experiment", "SELECT * FROM experiments WHERE id = $1", ["$json.id"], 560, pg_y(1)), pg_node("Create Experiment", "INSERT INTO experiments (id, title, template, subject_info_template) " "VALUES (gen_random_uuid(), $1, '[]'::jsonb, '[]'::jsonb) RETURNING *", ["$json.title"], 560, pg_y(2)), pg_node("Update Experiment", "UPDATE experiments SET title = $1 WHERE id = $2 RETURNING *", ["$json.title", "$json.id"], 560, pg_y(3)), pg_node("Delete Experiment", "DELETE FROM experiments WHERE id = $1 RETURNING id", ["$json.id"], 560, pg_y(4)), ] pairs = [(t["name"], 0, sw["name"])] + \ [(sw["name"], i, ops[i]["name"]) for i in range(len(ops))] return make_workflow("ExpDB · Experiments", t, sw, ops, build_connections(pairs)) # ══════════════════════════════════════════════════════════════════════════════ # 2. ANIMALS # ══════════════════════════════════════════════════════════════════════════════ def wf_animals(): t = trigger_node(60, 0) sw = switch_node("Route", ["list","get","create","update","delete"], 300, 0) ops = [ pg_node("List Animals", "SELECT a.*, COUNT(ds.id)::int AS daily_status_count " "FROM animals a " "LEFT JOIN daily_statuses ds ON ds.animal_id = a.id " "WHERE a.experiment_id = $1 " "GROUP BY a.id ORDER BY a.animal_name", ["$json.experiment_id"], 560, pg_y(0)), pg_node("Get Animal", "SELECT a.*, e.title AS experiment_title " "FROM animals a JOIN experiments e ON e.id = a.experiment_id " "WHERE a.id = $1", ["$json.id"], 560, pg_y(1)), pg_node("Create Animal", "INSERT INTO animals (id, experiment_id, animal_id_string, animal_name, subject_info) " "VALUES (gen_random_uuid(), $1, $2, $3, $4::jsonb) RETURNING *", ["$json.experiment_id", "$json.animal_id_string", "$json.animal_name", "JSON.stringify($json.subject_info ?? {})"], 560, pg_y(2)), pg_node("Update Animal", "UPDATE animals SET animal_id_string=$1, animal_name=$2, subject_info=$3::jsonb " "WHERE id=$4 RETURNING *", ["$json.animal_id_string", "$json.animal_name", "JSON.stringify($json.subject_info ?? {})", "$json.id"], 560, pg_y(3)), pg_node("Delete Animal", "DELETE FROM animals WHERE id = $1 RETURNING id", ["$json.id"], 560, pg_y(4)), ] pairs = [(t["name"], 0, sw["name"])] + \ [(sw["name"], i, ops[i]["name"]) for i in range(len(ops))] return make_workflow("ExpDB · Animals", t, sw, ops, build_connections(pairs)) # ══════════════════════════════════════════════════════════════════════════════ # 3. DAILY STATUSES # ══════════════════════════════════════════════════════════════════════════════ def wf_daily_statuses(): t = trigger_node(60, 0) sw = switch_node("Route", ["list","get","create","update","delete"], 300, 0) ops = [ pg_node("List Daily Statuses", "SELECT ds.id, ds.date, ds.experiment_description, ds.vitals, " "ds.treatment, ds.notes, ds.custom_fields, ds.analysis_summary, " "a.animal_name, a.animal_id_string " "FROM daily_statuses ds JOIN animals a ON a.id = ds.animal_id " "WHERE ds.animal_id = $1 ORDER BY ds.date DESC", ["$json.animal_id"], 560, pg_y(0)), pg_node("Get Daily Status", "SELECT ds.*, a.animal_name, a.animal_id_string, " "a.experiment_id, e.title AS experiment_title " "FROM daily_statuses ds " "JOIN animals a ON a.id = ds.animal_id " "JOIN experiments e ON e.id = a.experiment_id " "WHERE ds.id = $1", ["$json.id"], 560, pg_y(1)), pg_node("Create Daily Status", "INSERT INTO daily_statuses " "(id, animal_id, date, experiment_description, vitals, treatment, notes, custom_fields) " "VALUES (gen_random_uuid(), $1, $2::date, $3, $4, $5, $6, $7::jsonb) RETURNING *", ["$json.animal_id", "$json.date", "$json.experiment_description ?? null", "$json.vitals ?? null", "$json.treatment ?? null", "$json.notes ?? null", "JSON.stringify($json.custom_fields ?? {})"], 560, pg_y(2)), pg_node("Update Daily Status", "UPDATE daily_statuses SET " "experiment_description=$1, vitals=$2, treatment=$3, notes=$4, " "custom_fields=$5::jsonb " "WHERE id=$6 RETURNING *", ["$json.experiment_description ?? null", "$json.vitals ?? null", "$json.treatment ?? null", "$json.notes ?? null", "JSON.stringify($json.custom_fields ?? {})", "$json.id"], 560, pg_y(3)), pg_node("Delete Daily Status", "DELETE FROM daily_statuses WHERE id = $1 RETURNING id", ["$json.id"], 560, pg_y(4)), ] pairs = [(t["name"], 0, sw["name"])] + \ [(sw["name"], i, ops[i]["name"]) for i in range(len(ops))] return make_workflow("ExpDB · Daily Statuses", t, sw, ops, build_connections(pairs)) # ══════════════════════════════════════════════════════════════════════════════ # 4. DAILY ANALYSES (read + summary-push) # ══════════════════════════════════════════════════════════════════════════════ def wf_daily_analyses(): t = trigger_node(60, 0) sw = switch_node("Route", ["list","get","push_summary"], 300, 0) ops = [ pg_node("List Analyses", "SELECT id, daily_status_id, created_at, file_name, " "timestamp_col, note_col, total_note_rows, session_end_ts " "FROM daily_analyses WHERE daily_status_id = $1 ORDER BY created_at DESC", ["$json.daily_status_id"], 560, pg_y(0)), pg_node("Get Analysis", "SELECT * FROM daily_analyses WHERE id = $1", ["$json.id"], 560, pg_y(1)), pg_node("Push Analysis Summary", "UPDATE daily_statuses SET analysis_summary = $1::jsonb WHERE id = $2 RETURNING id, analysis_summary", ["JSON.stringify($json.analysis_summary)", "$json.daily_status_id"], 560, pg_y(2)), ] pairs = [(t["name"], 0, sw["name"])] + \ [(sw["name"], i, ops[i]["name"]) for i in range(len(ops))] return make_workflow("ExpDB · Daily Analyses", t, sw, ops, build_connections(pairs)) # ══════════════════════════════════════════════════════════════════════════════ # 5. SESSION FILES # ══════════════════════════════════════════════════════════════════════════════ def wf_session_files(): t = trigger_node(60, 0) sw = switch_node("Route", ["list","get"], 300, 0) ops = [ pg_node("List Session Files", "SELECT * FROM session_files WHERE daily_status_id = $1 ORDER BY created_at DESC", ["$json.daily_status_id"], 560, pg_y(0)), pg_node("Get Session File", "SELECT * FROM session_files WHERE id = $1", ["$json.id"], 560, pg_y(1)), ] pairs = [(t["name"], 0, sw["name"])] + \ [(sw["name"], i, ops[i]["name"]) for i in range(len(ops))] return make_workflow("ExpDB · Session Files", t, sw, ops, build_connections(pairs)) # ══════════════════════════════════════════════════════════════════════════════ # 6. AUDIT LOGS # ══════════════════════════════════════════════════════════════════════════════ def wf_audit_logs(): t = trigger_node(60, 0) sw = switch_node("Route", ["list_by_record","list_by_table"], 300, 0) ops = [ pg_node("List Logs by Record", "SELECT * FROM audit_logs " "WHERE table_name = $1 AND record_id = $2 " "ORDER BY timestamp DESC LIMIT $3", ["$json.table_name", "$json.record_id", "$json.limit ?? 50"], 560, pg_y(0)), pg_node("List Logs by Table", "SELECT * FROM audit_logs " "WHERE table_name = $1 " "ORDER BY timestamp DESC LIMIT $2", ["$json.table_name", "$json.limit ?? 100"], 560, pg_y(1)), ] pairs = [(t["name"], 0, sw["name"])] + \ [(sw["name"], i, ops[i]["name"]) for i in range(len(ops))] return make_workflow("ExpDB · Audit Logs", t, sw, ops, build_connections(pairs)) # ══════════════════════════════════════════════════════════════════════════════ # BONUS: Cross-subject report — one query to summarise all subjects in an experiment # ══════════════════════════════════════════════════════════════════════════════ def wf_reports(): t = trigger_node(60, 0) sw = switch_node("Route", ["subjects_summary","daily_summary","recent_activity"], 300, 0) ops = [ pg_node("Subjects Summary", "SELECT a.id, a.animal_name, a.animal_id_string, a.subject_info, " "COUNT(ds.id)::int AS total_days, " "MAX(ds.date) AS last_entry, " "COUNT(ds.analysis_summary)::int AS days_with_metrics " "FROM animals a " "LEFT JOIN daily_statuses ds ON ds.animal_id = a.id " "WHERE a.experiment_id = $1 " "GROUP BY a.id ORDER BY a.animal_name", ["$json.experiment_id"], 560, pg_y(0)), pg_node("Daily Summary", "SELECT ds.date, a.animal_name, " "ds.analysis_summary->>'total' AS total_attempts, " "ds.analysis_summary->>'success_rate' AS success_rate " "FROM daily_statuses ds JOIN animals a ON a.id = ds.animal_id " "WHERE a.experiment_id = $1 AND ds.analysis_summary IS NOT NULL " "ORDER BY ds.date, a.animal_name", ["$json.experiment_id"], 560, pg_y(1)), pg_node("Recent Activity", "SELECT al.timestamp, al.table_name, al.action, al.record_id, al.changes " "FROM audit_logs al " "ORDER BY al.timestamp DESC LIMIT $1", ["$json.limit ?? 50"], 560, pg_y(2)), ] pairs = [(t["name"], 0, sw["name"])] + \ [(sw["name"], i, ops[i]["name"]) for i in range(len(ops))] return make_workflow("ExpDB · Reports", t, sw, ops, build_connections(pairs)) # ── Main ─────────────────────────────────────────────────────────────────────── if __name__ == "__main__": workflows = [ wf_experiments(), wf_animals(), wf_daily_statuses(), wf_daily_analyses(), wf_session_files(), wf_audit_logs(), wf_reports(), ] print(f"Creating {len(workflows)} workflows…\n") for wf in workflows: post_workflow(wf) print("\nDone.")