Skip to content

Agent Execution Patterns ​

The WebRobot platform has two distinct ways to concretize an agent — to turn a description into a real running process with allocated resources and produced output:

  • Spark for batch data pipelines (the historical path)
  • Ray + agentic runtime for LLM-driven multi-step flows (the new path)

These two executors compose in four practical patterns. Picking the right one for your use case is an architectural decision, not a technology constraint. This page describes each pattern, when to use it, and how to express it through the API.

What "concretize" means here ​

A WebRobot agent row is, by itself, just a description. It only becomes a running thing when something provisions a job out of it:

Field on agentsProvisioned byBecomes
pipeline_yamlSparkApplicationOperatorSpark driver + N executor pods
agentic_profile_id → agentic_profiles.specKubeRay operatorRayJob + N actors in the cluster

The two columns are independent. An agent can have one, the other, both, or neither. The four patterns below cover the meaningful combinations.

The four patterns ​

Pattern 1 — Spark only ​

The classical ETL agent. The pipeline YAML fully describes the transformation; no LLM at runtime.

yaml
# agent row in the DB
pipeline_yaml: |
  fetch: { url: "https://books.toscrape.com/" }
  pipeline:
    - stage: wget
      args: ["${INPUT_URL}"]
    - stage: wgetExplore
      args: ["article.product_pod h3 a", 1, 10]
    - stage: extract
      args:
        - { selector: ".product_main h1", method: "text", as: "title" }
        - { selector: ".price_color",      method: "text", as: "price" }
  output:
    format: parquet
    path: "${OUTPUT_PARQUET_PATH}"
agentic_profile_id: null

Execution:

bash
POST /webrobot/api/projects/{projectId}/jobs/{jobId}/execute

This is the existing Spark execution path. Use it when:

  • the scraping logic is stable (selectors known)
  • the workload is bulk / batch (1000s of pages per run)
  • output is a dataset, not a decision

Pattern 2 — Agentic only ​

The new path. The agentic profile describes a multi-crew DAG; the Ray runtime executes it and returns a structured result.

yaml
# agent row in the DB
pipeline_yaml: null
agentic_profile_id: 17    # → references agentic_profiles row

# agentic_profiles[17].spec (excerpt)
profile: selector-grounding
crews:
  - id: grounder
    process: sequential
    agents:
      - id: selector_grounder
        role: "CSS Selector Grounder"
        tools: [browser, jersey]
    tasks:
      - id: ground
        description: "Visit seed_url, return verified selectors per field"

Execution:

bash
POST /webrobot/api/agents/{agentId}/execute
{
  "inputs": {
    "goal": "Extract title and price from books.toscrape",
    "seed_url": "https://books.toscrape.com/",
    "fields": [...]
  }
}

The Ray actor DAG runs and returns a dict — typically a structured result the caller consumes directly. No Spark involved. Use it when:

  • the work is decision-based (which selector? which intent?)
  • output is a result or a recommendation, not a dataset
  • the run is short (seconds to a few minutes)
  • examples: selector grounding, intent classification, RAG-backed Q&A, browser actions

Pattern 3 — Agentic drafts, Spark concretizes ​

A two-step flow where the agentic side produces a pipeline YAML as its output, and the user (or a downstream job) submits that YAML to Spark in a separate execution.

yaml
# starting state — agent has only an agentic profile that emits Spark YAML
agent.pipeline_yaml: null
agent.agentic_profile_id: <profile id of "pipeline-generator">

# the profile's DAG ends with an assembler crew producing a pipeline:
crews:
  - id: assembler
    agents: [yaml_writer]
    tasks:
      - id: assemble
        expected_output: "valid pipeline YAML, no prose"

Two executions, sequenced by the user:

bash
# step 1 — generate the YAML
POST /webrobot/api/agents/{agentId}/execute
{ "inputs": {"goal": "scrape arxiv cs.AI listings"} }
# → result.pipeline_yaml = "fetch: ...\npipeline: ..."

# step 2 — persist the generated YAML onto the agent
PUT /webrobot/api/agents/{agentId}
{ "pipeline_yaml": "<the YAML from step 1>" }

# step 3 — run as Spark
POST /webrobot/api/projects/{pid}/jobs/{jid}/execute

Use it when:

  • you want LLM-assisted scaffolding for a Spark pipeline
  • the generated YAML is the artifact, reusable across runs
  • the Spark execution will happen many times (batch, scheduled) — the agentic cost is paid once at generation time, not per run

This is what the demo POST /webrobot/api/demo/generate-pipeline flow does today (see Demo API).

Pattern 4 — Agentic orchestrates Spark from inside ​

The agentic flow itself decides when to invoke Spark, treating Spark as one of its tools. The orchestration is dynamic — Spark may run zero, one, or many times depending on the LLM's decisions inside the loop.

yaml
# agent row
agent.pipeline_yaml: null
agent.agentic_profile_id: <X>

# profile spec — note jersey_tool_actor exposes Manifest + Job APIs
tools:
  - id: jersey
    backend_actor: jersey_tool_actor
    endpoints:
      apply_manifest: "POST /webrobot/api/manifest/apply"
      execute_job:    "POST /webrobot/api/projects/{pid}/jobs/{jid}/execute"
      job_status:     "GET  /webrobot/api/projects/{pid}/jobs/{jid}/executions/{eid}/status"

crews:
  - id: analyst
    agents:
      - id: investigator
        role: "Data investigator"
        tools: [rag, jersey]
        # this agent will decide WITHIN the loop whether to fire Spark

Single execution, dynamic Spark involvement:

bash
POST /webrobot/api/agents/{agentId}/execute
{
  "inputs": {
    "goal": "Track sentiment for company X. If recent drop > 20%, do a full news scrape."
  }
}

What happens inside:

  1. RayJob spawns, CrewActor takes the goal
  2. investigator queries RAG → finds historical sentiment
  3. LLM decides: "drop is 25%, scrape needed"
  4. investigator calls jersey_tool_actor.apply_manifest(<news-scrape pipeline>)
  5. investigator calls jersey_tool_actor.execute_job(...) → Spark runs
  6. investigator polls jersey_tool_actor.job_status(...) until done
  7. Result: combined sentiment + scraped news, returned as the agent's output

Use it when:

  • the decision to invoke Spark is data-dependent (can only be made at runtime)
  • one user goal may trigger zero, one, or many Spark runs
  • the agentic flow's job is the orchestration, not the data work

How POST /agents/{id}/execute dispatches ​

The unified entry point inspects what the agent has configured and routes accordingly:

POST /webrobot/api/agents/{id}/execute
  │
  ├─ agent.agentic_profile_id is set?
  │    └─ YES → build StartRequest, submit RayJob
  │            (the profile decides if/when Spark is touched internally)
  │
  └─ NO → return 400 "no executor configured"
          with a hint pointing to the Spark path
          (POST /projects/{pid}/jobs/{jid}/execute)

Pattern 1 stays on its existing project-job-scoped Spark endpoint. Patterns 2-4 all flow through /agents/{id}/execute. The current dispatcher folds Spark in only via the project/job path; folding it directly under /agents/{id}/execute (so the same URL serves all four patterns) is a planned cleanup but not in place yet.

Choosing the pattern ​

QuestionSuggests
Selectors stable, batch scheduled?Pattern 1 (Spark)
LLM has to decide, output is a result not a dataset?Pattern 2 (agentic only)
LLM produces something to run many times later?Pattern 3 (agentic drafts, Spark concretizes)
LLM decides on-the-fly whether to run heavy data work?Pattern 4 (agentic orchestrates Spark)

If you genuinely can't tell which fits, start with Pattern 2 — it's the cheapest to iterate and the result will tell you whether you need Spark for scale (move to 3) or dynamic invocation (move to 4).

What about a kind field? ​

We considered adding agent.kind as an enum (spark / agentic / agentic_drafts_spark / agentic_orchestrates) to make the pattern explicit at the schema level. We chose not to today:

  • The patterns are still settling. Pattern 5+ (an agent that generates a Python extension to graft onto an existing pipeline; an agent that ends in a long-running streaming Spark job; etc.) may emerge soon and we don't want to keep migrating an enum.
  • The dispatcher only needs to look at which fields are populated. Distinguishing Pattern 3 vs 4 at the schema level adds no behaviour today — both are agentic_profile_id set, the difference is purely in the profile's spec contents.
  • UIs can present the pattern by inspecting the profile spec for the presence of jersey-tool stages that invoke Spark, or for a final pipeline-YAML deliverable. No schema flag needed.

This is a "watch and decide later" stance. If in a quarter the patterns crystallize and 80% of agents fall into one of two, we add the enum then.

Released under the MIT License.