<?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: Ale Guro</title>
    <description>The latest articles on DEV Community by Ale Guro (@ale_guro_f978e86cfa6ff51f).</description>
    <link>https://dev.to/ale_guro_f978e86cfa6ff51f</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1666799%2Fdfe7c624-4624-433f-a0f6-30eeab5aaf22.jpg</url>
      <title>DEV Community: Ale Guro</title>
      <link>https://dev.to/ale_guro_f978e86cfa6ff51f</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ale_guro_f978e86cfa6ff51f"/>
    <language>en</language>
    <item>
      <title>ODSL: A Ruby DSL for Service Objects That LLMs Can Actually Understand</title>
      <dc:creator>Ale Guro</dc:creator>
      <pubDate>Wed, 28 Oct 2026 18:13:10 +0000</pubDate>
      <link>https://dev.to/ale_guro_f978e86cfa6ff51f/odsl-a-ruby-dsl-for-service-objects-that-llms-can-actually-understand-5036</link>
      <guid>https://dev.to/ale_guro_f978e86cfa6ff51f/odsl-a-ruby-dsl-for-service-objects-that-llms-can-actually-understand-5036</guid>
      <description>&lt;p&gt;There's a pattern I've written — and seen written — hundreds of times in Ruby codebases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="vi"&gt;@user&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;
    &lt;span class="vi"&gt;@items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;
    &lt;span class="n"&gt;catalog&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Inventory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Pricing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;calculate_total&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;catalog&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="vi"&gt;@items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="vi"&gt;@user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;order&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;user: &lt;/span&gt;&lt;span class="vi"&gt;@user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;total: &lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;payment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;charge_customer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="kp"&gt;private&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;calculate_total&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;catalog&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;charge_customer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works. Until it doesn't. Until &lt;code&gt;call&lt;/code&gt; is 150 lines long, private helpers have side effects that depend on implicit ordering, and when a payment processor times out on step 4 of 7, you have no idea which steps completed.&lt;/p&gt;

&lt;p&gt;I wrote &lt;strong&gt;&lt;a href="https://github.com/aleguro/ODSL" rel="noopener noreferrer"&gt;ODSL&lt;/a&gt;&lt;/strong&gt; to fix this — and what I didn't anticipate was how naturally it aligns with the way LLMs generate code.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem with &lt;code&gt;def call&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Service objects are supposed to bring clarity. Instead they often just relocate complexity.&lt;/p&gt;

&lt;p&gt;The real issues:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Unnamed intermediate results&lt;/strong&gt; — &lt;code&gt;total&lt;/code&gt;, &lt;code&gt;order&lt;/code&gt;, and &lt;code&gt;payment&lt;/code&gt; are local variables. No audit trail, no checkpoints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No resumability&lt;/strong&gt; — if step 4 fails, you start over from step 1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hidden coupling&lt;/strong&gt; — private methods call other private methods in a fragile, implicit order.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Opaque in stack traces&lt;/strong&gt; — when something breaks, the trace says &lt;code&gt;call&lt;/code&gt;, not "it failed at the payment step."&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  What ODSL Does Differently
&lt;/h2&gt;

&lt;p&gt;ODSL makes each step &lt;strong&gt;named&lt;/strong&gt;, &lt;strong&gt;recorded&lt;/strong&gt;, and &lt;strong&gt;resumable&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_order&lt;/span&gt;
  &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Inventory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Pricing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:catalog&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:catalog&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:calculate_total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:order_total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:order_total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;instantiate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:charge_customer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:payment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;finish_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:payment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read that out loud. It sounds like how you'd &lt;em&gt;describe&lt;/em&gt; the workflow to a colleague:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Build an inventory object with Pricing. Store it as catalog. With the catalog, items, and user, calculate the total. Store that as order_total. With the user and order total, create an Order. Charge the customer. Return the order and payment."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's not an accident.&lt;/p&gt;




&lt;h2&gt;
  
  
  The LLM Angle: Why This Matters Now
&lt;/h2&gt;

&lt;p&gt;When you ask an LLM to generate a service object, it will describe the business logic in natural language first — then translate it to code. The translation is where context is lost.&lt;/p&gt;

&lt;p&gt;With a traditional service object, the LLM has to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Infer which local variables flow into which private methods&lt;/li&gt;
&lt;li&gt;Guess the implicit ordering of side effects&lt;/li&gt;
&lt;li&gt;Fill in missing context about what each step produces&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With ODSL, &lt;strong&gt;the code is already written the way an LLM thinks about it&lt;/strong&gt;. Each line is a declarative sentence. Each &lt;code&gt;.get(:var)&lt;/code&gt; names what was produced. Each &lt;code&gt;with(:a, :b).make(:do_thing)&lt;/code&gt; names exactly what flows in and what happens next.&lt;/p&gt;

&lt;p&gt;When I started using AI assistants to write ODSL services, I noticed something: the LLM barely needed prompting. I'd describe the workflow in plain English:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Build a game object from Game and Player modules. Get it as game_object. With the player, game, and game_object, call new_game. Store it as game_instance. Instantiate a ScoreBoard with it."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And the generated code was nearly exactly right on the first try — because the DSL mirrors the description.&lt;/p&gt;

&lt;p&gt;Contrast that with generating a traditional service: you have to prompt carefully about which variables go where, remind the model not to introduce hidden state, and then review for implicit ordering bugs. The abstraction gap between natural language and Ruby code is much wider.&lt;/p&gt;




&lt;h2&gt;
  
  
  Production Features
&lt;/h2&gt;

&lt;p&gt;ODSL isn't just syntactic sugar. v0.3.0 added infrastructure for real production use:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Idempotency + step-level audit trail:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:process_order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;key: &lt;/span&gt;&lt;span class="s2"&gt;"order-&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Safe to call twice — no-op if already completed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Odsl&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Audit&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ExecutionContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;idempotency_key: &lt;/span&gt;&lt;span class="s2"&gt;"order-42"&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="nf"&gt;status&lt;/span&gt;        &lt;span class="c1"&gt;# =&amp;gt; "completed"&lt;/span&gt;
&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;step_records&lt;/span&gt;  &lt;span class="c1"&gt;# =&amp;gt; per-step audit trail&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Resume failed runs from the first incomplete step:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;OrderService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;key: &lt;/span&gt;&lt;span class="s2"&gt;"order-42"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Skips completed steps, re-runs from where it died&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Distributed locking (fail-fast, no waiting):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process_order&lt;/span&gt;
  &lt;span class="n"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:order_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# raises LockError immediately if already held&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;lock&lt;/code&gt; is a no-op when Redis isn't configured, so the same code works in development without any infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Background execution:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_in_background&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:generate_report&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_in_parallel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:invoice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:notify&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All opt-in. No breaking changes.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Design Principle
&lt;/h2&gt;

&lt;p&gt;The core insight behind ODSL: &lt;strong&gt;code that reads like natural language is easier for both humans and machines to reason about&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Named steps create shared vocabulary between you, your team, and your AI assistant. When you &lt;code&gt;.get(:order_total)&lt;/code&gt;, that name appears in audit logs, stack traces, and error messages. Everyone — human or model — is speaking the same language.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;.get(:var)&lt;/code&gt; pattern in particular is what makes LLM-assisted development click. When you prompt an LLM with an ODSL service, you can point to each named result by name, ask it to extend a specific step, or debug a specific checkpoint. There's no ambiguity about what &lt;code&gt;order_total&lt;/code&gt; is or where it came from.&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;gem &lt;span class="nb"&gt;install &lt;/span&gt;odsl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or in your Gemfile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s2"&gt;"odsl"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/aleguro/ODSL" rel="noopener noreferrer"&gt;github.com/aleguro/ODSL&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;RubyGems: &lt;a href="https://rubygems.org/gems/odsl" rel="noopener noreferrer"&gt;rubygems.org/gems/odsl&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're building Ruby service objects and experimenting with AI-assisted development, I'd love to hear how ODSL fits into that workflow.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>ruby</category>
      <category>programming</category>
      <category>llm</category>
    </item>
  </channel>
</rss>
