THOUGHT LEADERSHIP

Removing the Code Without Adding Drag-and-Drop

August 15, 2026 9 min read

FireFoundry Team

Engineering

There is a real question hiding under the low-code debate, and it is usually asked badly. The good version is: how much of what you write when you build an agent is your business logic, and how much is ceremony?

For a large class of agents the honest answer is that most of it is ceremony. Declare an entity. Wire a bot to a prompt. Define a workflow that calls the bot and stores the result. None of that is where your product differs from anyone else's.

Why drag-and-drop was the wrong fix

Visual builders identified the right problem and solved it with the best tool available at the time. If writing boilerplate is tedious, let people assemble it by dragging boxes.

The cost always showed up later, and it was always the same cost. You cannot diff a canvas. You cannot review a pull request against it. You cannot express the one thing the builder did not anticipate without escaping into a code block the builder then treats as opaque. And you cannot leave, because what you built is not portable to anything.

Low-code was made obsolete not by a better builder but by AI coding assistants. Describing what you want and getting real, maintainable code back is strictly more powerful than dragging boxes — and the output is something an engineer can review and extend.

Which leaves a gap worth filling. If assistants are generating the code anyway, the boilerplate does not need to be ergonomic for humans to type. It needs to be precise, reviewable, and easy to generate.

Four DSLs, one bundle

What follows is from a content-analysis bundle running on FireFoundry with no hand-written TypeScript entity or bot code at all. Four file types do the work.

BundleML declares what exists

The bundle file names the entities and bots, and defines the HTTP surface. Note the endpoint handler: when you do need imperative code, it sits inline in CDATA rather than in a separate build.

<bundle id="558707ed-d548-4a68-a384-1bf18f6da676"
        name="XML DSL Content Analyzer"
        description="Zero-TypeScript content analysis bundle demonstrating all four FireFoundry XML DSLs">
  <config>
    <port>3000</port>
  </config>
  <constructors>
    <entity type="AnalysisWorkflow" ref="analysis-workflow.agentml"/>
    <bot type="AnalyzerBot" ref="analyzer-bot.botml"/>
  </constructors>
  <endpoints>
    <endpoint route="run-analysis" method="POST" response-type="json">
      <handler><![CDATA[
        const { text, analysis_type, requested_by } = body;
        if (!text) throw new Error('text is required');
        const node = await entity_factory.create_entity_node({
          app_id,
          specific_type_name: 'AnalysisWorkflow',
          general_type_name: 'AnalysisWorkflow',
          data: { topic: text, analysis_type: analysis_type || 'general' }
        });
        const entity = await entity_factory.get_entity(node.id);
        entity.runBackground();
        return { entity_id: node.id, status: 'pending' };
      ]]></handler>
    </endpoint>
  </endpoints>
</bundle>

AgentML defines the workflow

Steps, not code. Yield a status so callers can watch progress, call a bot, write to working memory, append to the entity graph. The <expr> element is the escape hatch for the cases where you need a real expression.

<agent id="AnalysisWorkflow">
  <static-args>
    <arg name="topic" type="string"/>
    <arg name="analysis_type" type="string"/>
    <arg name="requested_by" type="string"/>
  </static-args>
  <run-impl>
    <yield-status message="Starting content analysis workflow"/>
    <yield-status message="Calling AnalyzerBot"/>

    <call-bot name="AnalyzerBot" result="analysis_result">
      <arg name="input" value="args"/>
      <arg name="topic" value="args.topic"/>
      <arg name="analysis_type" value="args.analysis_type"/>
      <arg name="requested_by" value="args.requested_by"/>
      <arg name="mode">"detailed"</arg>
    </call-bot>

    <yield-status message="Saving results to working memory"/>

    <wm-set key="analysis/latest-result" value="analysis_result"/>
    <wm-set key="analysis/latest-topic" value="args.topic"/>

    <yield-status message="Updating entity graph"/>

    <graph-append edge-type="ProducedAnalysis" target="self">
      <data>
        <field name="topic" value="args.topic"/>
        <field name="timestamp">
          <expr>new Date().toISOString()</expr>
        </field>
      </data>
    </graph-append>
  </run-impl>
</agent>

BotML configures the model and the prompt

Model pool, temperature, a semantic label for telemetry, and a structured prompt group. The interesting part is <if condition>: prompt logic is declarative too, so a conditional instruction is visible in the file rather than buried in string concatenation.

<bot id="AnalyzerBot" name="AnalyzerBot" max-tries="2">
  <llm-options temperature="0.3">
    <model-pool>firebrand_completion_default</model-pool>
    <semantic-label>xml-dsl-analyzer</semantic-label>
  </llm-options>
  <structured-prompt-group>
    <base>
      <prompt role="system">
        <text>You are a content analysis assistant for the FireFoundry platform.</text>
        <text>Respond ONLY with a valid JSON object. Do NOT use markdown code blocks, backticks, or any prose. Start your response directly with { and end with }.</text>
        <text>The JSON object must have exactly these keys: summary (string), topic (string), sentiment (one of "positive", "negative", or "neutral"), findings (array of strings), confidence (number between 0 and 1).</text>
        <if condition="args.mode === 'detailed'">
          <text>Provide detailed analysis with explanations for each finding.</text>
        </if>
      </prompt>
    </base>
    <input>
      <prompt role="user">
        <text>Analyze the following content: {{input.topic}}</text>
        <section name="context">
          <text>Analysis type: {{input.analysis_type}}</text>
          <text>Requested by: {{input.requested_by}}</text>
        </section>
        <text>Return your analysis as a JSON object with keys: summary, topic, sentiment, findings (array), and confidence (number 0-1).</text>
      </prompt>
    </input>
  </structured-prompt-group>
</bot>

PromptML is the same prompt structure as a standalone file, for prompts shared across bots. Same <if>, same {{interpolation}}.

What you keep

This is what separates it from a builder. Markup is text, so everything you expect from text still works: it lives in version control, it diffs cleanly, it goes through code review, and an assistant can generate and modify it at least as reliably as it generates code — arguably more so, because the schema constrains the output.

Look again at the AgentML above and consider what reviewing that change costs versus reviewing the same workflow expressed as a hundred lines of orchestration. The steps are the diff.

When to drop back to TypeScript

When the logic is the product. Declarative form is right for the shape most bundles share — take input, call a model, persist the result, expose an endpoint. It is wrong the moment your workflow branches on domain rules that deserve real tests, or you are integrating a system with genuine edge cases.

The escape is not a hatch bolted onto a builder. It is the same platform's primary interface: the TypeScript SDK, which the declarative layer is built on top of rather than beside. A bundle can be declarative today and partly TypeScript next quarter without being rewritten.

The line we are drawing

Declarative where declaration genuinely fits. Full programmatic control where it does not. No visual builder in either case, and nothing that cannot be read, reviewed, and version-controlled as text.

We removed the code without adding drag-and-drop. That is the design goal, and it is a narrower claim than "anyone can build an agent" — deliberately so. Someone still has to know what the agent should do.

The declarative bundle runtime is in active development. The bundle above runs today on our dev cluster; the DSLs are still moving.

More on the development model in Introducing FireFoundry, or start with the SDK via the developer resources.

FireFoundry Team

Product

The FireFoundry team builds enterprise infrastructure for AI agents. We are engineers, product thinkers, and operators who have spent years building and scaling production AI systems. Our mission is to close the gap between AI prototypes and production-grade software.

Related Posts

Request Beta Access

FireFoundry is now in private beta. Join the teams who decided to buy their AI operating environment rather than assemble one.