Skip to Content
We are live but in Staging 🎉
ExamplesAgentic workflow

Agentic workflow

Scriptum  is Dodil’s typed workflow and agent engine. You write a workflow as a .scriptum file — a recipe of steps that call tools, branch on conditions or LLM judgment, and run autonomous agents — and the compiler type-checks the whole program before anything runs. Inference (LLM chat, embeddings, reranking) is handled by Ignite ’s managed model service, so there are no model keys to wire up.

Scriptum is cloud-only. A run is a durable, resumable thread: the platform schedules it, persists its state after every step, and lets you read status, events, and results back. There is nothing to host and no execution mode to pick.

This example builds the same workflow two ways:

  • The CLI way — a person authors and runs it from the terminal with dodil.
  • The programmatic way — an app or agent drives the ScriptumService  API directly, authenticated with an IAM access token .

What you’ll build

A small “research triage” workflow. It takes a topic, searches the web for it, asks the managed LLM to judge how strong the findings are, and then branches on that LLM decision: strong results get summarized; weak results are flagged for a deeper pass. Either way it emits a structured result.

The pieces map directly onto Scriptum primitives:

Save this as research_triage.scriptum:

script "Research Triage" Search the web for a topic, judge the findings with an LLM, and branch: summarize strong results, flag weak ones. version "0.1.0" input topic : text -- The subject to research max_results : number = 10 -- How many search hits to pull output status : text -- "summarized" or "needs_deeper_research" topic : text summary : text import tools web_search from native do "Search the web" with web_search query = topic max_results = max_results -> results do "Judge the findings" with llm system = "You are a research analyst. Be concise and honest about gaps." prompt = "Topic: {topic}\n\nSearch results:\n{results}\n\nAre these findings strong enough to summarize?" -> verdict decide "Strong enough to summarize?" with llm "Based on this assessment, is the research strong enough to summarize, or does it need a deeper pass?\n\n{verdict.text}" "Summarize": do "Write the summary" with llm prompt = "Summarize the research on '{topic}' in three sentences:\n\n{results}" -> drafted emit "Summarized" status = "summarized" topic = topic summary = drafted.text "Needs deeper research": emit "Needs more" status = "needs_deeper_research" topic = topic summary = verdict.text

A few things worth noting, all from the language reference :

  • web_search is the only tool that needs an import tools entry — llm is an internal model capability and is invoked directly.
  • {topic} and {results} interpolate bindings into a prompt; verdict.text reads the .text field of the LLM completion.
  • Both decide branches bind the same output via emit, so the output contract is satisfied no matter which branch runs.

The CLI way

Authenticate once, then walk the script through its lifecycle: create → save → compile → publish → run.

# Authenticate (once); applies to every dodil product dodil auth login # browser (interactive) # or, for non-interactive / agent use, set service-account env vars and: # DODIL_SERVICE_ACCOUNT_ID=... DODIL_SERVICE_ACCOUNT_SECRET=... dodil auth login
# 1) Create the script container (metadata only — no content yet) dodil scriptum script create research-triage \ --description "Search, judge with an LLM, then branch" \ --tags "agent,research" # 2) Attach the .scriptum source as the draft dodil scriptum draft save research-triage -f ./research_triage.scriptum # 3) Type-check the draft against each tool's schema dodil scriptum draft compile research-triage

compile resolves every tool, checks types and field access, and reports diagnostics with line and column. Fix anything it flags before continuing. (You can also dodil scriptum draft test research-triage --input '{"topic":"..."}' to run the draft once without publishing.)

# 4) Publish the compiled draft as an immutable, numbered version dodil scriptum draft publish research-triage # 5) Run it as a cloud thread and print the final output dodil scriptum thread run research-triage \ --input '{"topic":"durable workflow engines","max_results":10}' \ --result

thread run creates a thread, follows it live (streaming progress, including which branch the decide chose), and --result prints the final output when it reaches a terminal state. A thread is durable — fetch it later with dodil scriptum thread get <id> or dodil scriptum thread result <id>.

The programmatic way (Python)

An app or agent drives the same lifecycle over the ScriptumService HTTP API. The calls mirror the CLI exactly: CreateScript → SaveScriptDraft → CompileScriptDraft → PublishScriptDraft → CreateThread, then poll GetThread and read GetThreadResult.

Authenticate with an IAM access token  as a bearer token, and pass your org with the x-organization-name header. The script body goes in dslContent; thread input goes in inputJson (a JSON string). HTTP bodies are pbjson (camelCase).

import json import os import time import requests BASE = "https://api.dev.dodil.io" # staging HTTP gateway HEADERS = { "Authorization": f"Bearer {os.environ['DODIL_TOKEN']}", # IAM access token "x-organization-name": os.environ["DODIL_ORG"], "Content-Type": "application/json", } SCRIPT = "research-triage" DSL = r""" script "Research Triage" Search the web for a topic, judge the findings with an LLM, and branch: summarize strong results, flag weak ones. version "0.1.0" input topic : text -- The subject to research max_results : number = 10 -- How many search hits to pull output status : text topic : text summary : text import tools web_search from native do "Search the web" with web_search query = topic max_results = max_results -> results do "Judge the findings" with llm system = "You are a research analyst. Be concise and honest about gaps." prompt = "Topic: {topic}\n\nSearch results:\n{results}\n\nAre these findings strong enough to summarize?" -> verdict decide "Strong enough to summarize?" with llm "Based on this assessment, is the research strong enough to summarize, or does it need a deeper pass?\n\n{verdict.text}" "Summarize": do "Write the summary" with llm prompt = "Summarize the research on '{topic}' in three sentences:\n\n{results}" -> drafted emit "Summarized" status = "summarized" topic = topic summary = drafted.text "Needs deeper research": emit "Needs more" status = "needs_deeper_research" topic = topic summary = verdict.text """ def post(path, body): r = requests.post(f"{BASE}{path}", headers=HEADERS, data=json.dumps(body)) r.raise_for_status() return r.json() def get(path): r = requests.get(f"{BASE}{path}", headers=HEADERS) r.raise_for_status() return r.json() # 1) Create the script (metadata only). Skip/ignore if it already exists. post("/v1/scriptum/scripts", { "name": SCRIPT, "description": "Search, judge with an LLM, then branch", "tags": ["agent", "research"], }) # 2) Save the .scriptum source as the draft post(f"/v1/scriptum/scripts/{SCRIPT}/draft", {"dslContent": DSL}) # 3) Compile (type-check). Bail out on diagnostics. compiled = post(f"/v1/scriptum/scripts/{SCRIPT}/draft/compile", {}) if not compiled.get("success"): for d in compiled.get("diagnostics", []): print(f" {d['severity']} {d['line']}:{d['col']} {d['message']}") raise SystemExit(f"compile failed: {compiled.get('error')}") # 4) Publish the compiled draft as a new active version published = post(f"/v1/scriptum/scripts/{SCRIPT}/draft/publish", {}) print("published version", published["version"]) # 5) Create a thread (version 0 = active). input_json is a JSON string. thread = post("/v1/scriptum/threads", { "scriptName": SCRIPT, "version": 0, "inputJson": json.dumps({"topic": "durable workflow engines", "max_results": 10}), }) thread_id = thread["threadId"] print("thread", thread_id, thread["status"]) # 6) Poll until the thread reaches a terminal state while True: t = get(f"/v1/scriptum/threads/{thread_id}") status = t["status"] if status in ("THREAD_STATUS_COMPLETED", "THREAD_STATUS_FAILED", "THREAD_STATUS_CANCELLED"): break time.sleep(1) if status != "THREAD_STATUS_COMPLETED": raise SystemExit(f"thread {status}: {t.get('error')}") # 7) Read the result. output_data is JSON, base64-encoded over HTTP. import base64 result = get(f"/v1/scriptum/threads/{thread_id}/result") output = json.loads(base64.b64decode(result["outputData"])) print(json.dumps(output, indent=2)) # -> { "status": "summarized", "topic": "...", "summary": "..." }

GetThreadResult is the unary read used here; for low-latency progress (status transitions and the branch the decide chose) stream WatchThread instead — it carries signals only, with content fetched via the result call. The same surface is available over gRPC at rpc.dev.dodil.io:443 (service dodil.scriptum.v1.ScriptumService) if you prefer a typed, streaming client.

See also