<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: 元方</title>
    <description>The latest articles on DEV Community by 元方 (@_34d3c9ee969f97a6c811fb).</description>
    <link>https://dev.to/_34d3c9ee969f97a6c811fb</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3987789%2F85c8cc5b-c0d1-491e-b3dd-4e6ff0e559e0.jpg</url>
      <title>DEV Community: 元方</title>
      <link>https://dev.to/_34d3c9ee969f97a6c811fb</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/_34d3c9ee969f97a6c811fb"/>
    <language>en</language>
    <item>
      <title>Building a self-hosted, AI-native workflow engine in Rust (180 node types, no SDK bloat)</title>
      <dc:creator>元方</dc:creator>
      <pubDate>Tue, 16 Jun 2026 18:03:59 +0000</pubDate>
      <link>https://dev.to/_34d3c9ee969f97a6c811fb/building-a-self-hosted-ai-native-workflow-engine-in-rust-180-node-types-no-sdk-bloat-266b</link>
      <guid>https://dev.to/_34d3c9ee969f97a6c811fb/building-a-self-hosted-ai-native-workflow-engine-in-rust-180-node-types-no-sdk-bloat-266b</guid>
      <description>&lt;p&gt;I've spent the last while building &lt;strong&gt;&lt;a href="https://github.com/bj-qizhi/trigix" rel="noopener noreferrer"&gt;Trigix&lt;/a&gt;&lt;/strong&gt; — an open-source (MIT), self-hostable workflow automation platform. Think n8n, but the execution engine is in Rust and the AI nodes can run entirely against local models. This post is about a few engineering decisions I think are worth sharing, not a feature tour.&lt;/p&gt;

&lt;h2&gt;
  
  
  The node model: one trait, ~180 implementations
&lt;/h2&gt;

&lt;p&gt;A workflow is a DAG of typed nodes. Every node is one variant of a &lt;code&gt;NodeType&lt;/code&gt; enum and one async function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="py"&gt;.node_type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nn"&gt;NodeType&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Http&lt;/span&gt;     &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;execute_http&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nn"&gt;NodeType&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Agent&lt;/span&gt;    &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;execute_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ai_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nn"&gt;NodeType&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Sqs&lt;/span&gt;      &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;execute_sqs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// … ~180 arms&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adding a node type touches a fixed set of places (enum variant, executor impl, dispatch arm, a &lt;code&gt;node_type → str&lt;/code&gt; map, and the frontend palette/config panel). It's mechanical, which is the point: the cost of a new integration is bounded and reviewable, and the compiler tells you if you forgot a touch point.&lt;/p&gt;

&lt;p&gt;The engine itself is the interesting part — async DAG scheduling with parallel fan-out/fan-in, retries, timeouts, cancellation, and sub-workflows — but the node catalog is where most of the surface area lives, so I optimized hard for "cheap to add, hard to get subtly wrong."&lt;/p&gt;

&lt;h2&gt;
  
  
  HTTP-first, so most "integrations" are just config + auth
&lt;/h2&gt;

&lt;p&gt;Most SaaS/DB/vector-store/cloud nodes are thin HTTP clients that return &lt;code&gt;{ status, body }&lt;/code&gt;. The value of a first-class node over "just use the generic HTTP node" is the curated config UI and auth handling, not raw capability. That framing kept ~150 of the nodes small and uniform.&lt;/p&gt;

&lt;p&gt;For cloud services I leaned on caller-supplied tokens where possible (e.g. GCS, Vertex AI, BigQuery, Snowflake all take a bearer token) instead of baking in each provider's OAuth dance. Honest trade-off: less magic, but no giant SDK per provider and no credential-exchange code to maintain.&lt;/p&gt;

&lt;h2&gt;
  
  
  The parts that &lt;em&gt;couldn't&lt;/em&gt; be plain HTTP — and how I kept the build clean
&lt;/h2&gt;

&lt;p&gt;A self-hosted tool has to build everywhere with &lt;code&gt;cargo build&lt;/code&gt;, with no "first install these system libraries" footnote. That constraint drove a few choices:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AWS (SQS/SNS/Bedrock).&lt;/strong&gt; Instead of pulling in the AWS SDK, I implemented &lt;strong&gt;Signature V4 from scratch&lt;/strong&gt; with the crypto crates already in the tree (&lt;code&gt;sha2&lt;/code&gt;/&lt;code&gt;hmac&lt;/code&gt;/&lt;code&gt;hex&lt;/code&gt;). The reassuring part: AWS publishes a SigV4 test suite, so the signer is unit-tested against the canonical &lt;code&gt;get-vanilla&lt;/code&gt; vector:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The signer must reproduce AWS's published signature exactly.&lt;/span&gt;
&lt;span class="nd"&gt;assert!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="nf"&gt;.ends_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s"&gt;"Signature=5fa00fa31553b73ebf1942676e86291e8372ff2a2260956d9b8aae1d763fbf31"&lt;/span&gt;
&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single test is worth more than a hundred round-trip mocks — it pins the canonical-request and signing-key derivation to a known-good answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SSH/SFTP.&lt;/strong&gt; The obvious crate (&lt;code&gt;ssh2&lt;/code&gt;) binds to libssh2 — a system dependency that breaks the build for anyone without the dev headers (and &lt;code&gt;cmake&lt;/code&gt;). So I used &lt;strong&gt;russh + russh-sftp&lt;/strong&gt;, a pure-Rust SSH implementation. &lt;code&gt;cargo build&lt;/code&gt; stays self-contained; password and private-key auth both work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SQL Server.&lt;/strong&gt; Same reasoning — &lt;code&gt;tiberius&lt;/code&gt; is a pure-Rust TDS driver, so the MSSQL node needs no native client.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OCR.&lt;/strong&gt; This one I &lt;em&gt;couldn't&lt;/em&gt; keep pure-Rust without a heavy native dependency, so instead of linking libtesseract at build time, the node shells out to the &lt;code&gt;tesseract&lt;/code&gt; CLI at runtime. The workspace still builds everywhere; OCR just needs the binary present when that node actually runs. Being explicit about that boundary beats a build that fails on a clean machine.&lt;/p&gt;

&lt;p&gt;The theme: a system dependency at &lt;strong&gt;build&lt;/strong&gt; time is a tax on every contributor and CI run; a dependency at &lt;strong&gt;runtime&lt;/strong&gt; is opt-in and only paid by the people who use that feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local-first AI
&lt;/h2&gt;

&lt;p&gt;The LLM/agent/RAG nodes speak the OpenAI-compatible wire format, so you can point them at a local &lt;strong&gt;Ollama&lt;/strong&gt; or &lt;strong&gt;vLLM&lt;/strong&gt; and run the whole AI side offline. RAG retrieval runs on your own Postgres/pgvector with hybrid (vector + full-text) search, optional reranking, and an HNSW index. Cloud providers are optional, not assumed — which matters if you're self-hosting precisely to avoid sending data out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.poc.yml up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That brings up the console/API, Postgres+pgvector, Redis, and an optional AI runtime. There's also a Helm chart on GHCR and a one-click GitHub Codespaces devcontainer if you want to poke at it before committing a VM.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Honest status:&lt;/strong&gt; it's young and I'm the primary author; 600+ tests and a v1.3.0 release, but treat it accordingly. And full transparency since it's relevant on this platform — I used AI coding assistants while building it; it's a real, tested codebase, not a one-shot generated repo.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/bj-qizhi/trigix" rel="noopener noreferrer"&gt;https://github.com/bj-qizhi/trigix&lt;/a&gt; — feedback on the engine design and the node model especially welcome.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>opensource</category>
      <category>selfhosted</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
