#development orchestration/workflow set-up

1 messages · Page 1 of 1 (latest)

wooden basalt
#

my goal is this:

  • 3 agents - an orchestrator, a builder, and qa
  • i can task orchestrator with a project name + spec - they will then build it out fully and store it on the filesystem
  • i can then talk to the orchestrator to create tasks in that project - they will take those, full spec them out and store it on the filesystem
    • followed by breaking the task up in to manageable steps, with clear goals and criterion, and each step also stored separately on filesystem
  • when i tell orchestrator to start a task, it'll delegate it to the builder agent as a subagent session, starting with the first step and going through all steps sequentially with the following flow:
    1. builder takes the spec file for the step and builds it out with tests
    2. when the builder is done, it will tell the orchestrator
    3. the orchestrator will then spawn the qa agent as a subagent to ensure the work matches the pass criterion from the step spec file
    4. qa will then report back to orchestrator on whether it passes or fails, pass = repeat for the next step of the task and fail = orchestrator throws it back to the builder to correct

the files for each task + step must be kept updated at every step matching a required format

having an llm maintain the various files from my testing is always unreliable - formatting changes, some things missed, etc. - i was building some tools for openclaw to solve this, but ideally i'd use something that already exists/other people are using

any suggestions?

wary kayakBOT
#

To help others find answers, you can mark your question as solved via Right click solution message -> Apps -> âś… Mark Solution

honest birchBOT
final anvilBOT
#

Yeah, I’d avoid “LLM directly edits canonical task files” as the core design. That’s exactly where drift starts.

What I’d use in OpenClaw today:

  • Task Flow for the durable orchestrator state machine
  • subagent/background tasks for builder + QA runs
  • a small plugin tool layer for all file/state mutations
  • optionally Lobster + llm-task if you want more deterministic pipelines

The big recommendation:

Make structured state the source of truth, not markdown/files the model hand-edits.

So instead of:

  • agent edits task.md
  • agent edits step-01.md
  • agent edits status.md
#

do:

  • plugin tool writes validated JSON/SQLite state
  • renderer generates the human-readable files from that state
  • orchestrator only uses tools like:
    • create_project
    • create_task
    • create_step
    • set_step_status
    • record_builder_result
    • record_qa_result

That gives you stable formatting, required fields, and much less “oops it forgot one field”.

For your exact setup, I’d say:

  1. Orchestrator owns project/task/step state
  2. Builder runs as spawned child work
#
  1. QA runs after each builder completion
  2. Task Flow tracks the whole sequence durably across restarts
  3. Plugin tools are the only thing allowed to mutate project/task/step artifacts

If you want three truly separate brains/workspaces, use multi-agent. If not, one orchestrator spawning focused builder/qa subagents is simpler.

Closest existing pieces:

So, short answer: yes, there are existing primitives, but not really a turnkey “project orchestrator/builder/qa filesystem contract” package yet. The reliable pattern is Task Flow + typed plugin tools + generated artifacts.

If you want, I can sketch a concrete schema/file layout for project.json, task.json, step.json, and the pass/fail loop.

wooden basalt
#

are there potentially any existing systems you're aware of that create this kind of thing? i.e. something someone has already built?

final anvilBOT
#

Yeah, a few, but I don’t know one that is a perfect drop-in for your exact “strict project/task/step filesystem contract” shape.

The closest buckets I know:

Closest to your idea

  • MetaGPT, https://github.com/FoundationAgents/MetaGPT
    Probably the closest philosophically. It already thinks in roles like PM / architect / engineer / QA and generates structured artifacts, tasks, docs, and a repo.
    Good fit if you want “AI software company” vibes.

Closest to coding-agent orchestration

Best frameworks if you’re still building it yourself

#

These are more “build your own orchestrator cleanly” than “finished product”.

More coding-platform than artifact-system

  • OpenHands, https://www.openhands.dev/
    Great coding-agent platform, but less naturally centered on your strict spec/task/step artifact model.

My honest take:

  • If you want the closest existing thing: MetaGPT
  • If you want best coding execution/orchestration: Agent Orchestrator or AWS CAO
  • If you want to build your own properly: LangGraph or AutoGen

The annoying bit is that even the better systems usually still get flaky if the LLM is directly maintaining canonical files. That problem is pretty universal.

#

So even if you adopt one of these, I’d still strongly recommend:

  • canonical state in JSON/DB
  • human-readable spec/task files as generated artifacts
  • agents only mutate state through typed tools

If you want, I can do a tighter comparison of MetaGPT vs CrewAI vs LangGraph vs “build it natively in OpenClaw” for your exact use case.

wooden basalt
#

give me an example of what a "create_task" typescript tool implementation with taskflow would look like

final anvilBOT
#

Yep, roughly like this.

This version does two things:

  1. creates the canonical task.json + steps/*.json files
  2. creates a managed Task Flow for that task, initially in an await-start / waiting state
import { randomUUID } from "node:crypto";
import { mkdir, writeFile } from "node:fs/promises";
import path from "node:path";
import { Type } from "@sinclair/typebox";
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";

function slugify(input: string): string {
  return input
#
    .toLowerCase()
    .trim()
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-+|-+$/g, "")
    .slice(0, 60);
}

function isoNow(ts = Date.now()): string {
  return new Date(ts).toISOString();
}

const StepInputSchema = Type.Object(
  {
    title: Type.String({ minLength: 1 }),
    goal: Type.String({ minLength: 1 }),
#
    passCriteria: Type.Array(Type.String({ minLength: 1 }), { minItems: 1 }),
    id: Type.Optional(Type.String({ minLength: 1 })),
  },
  { additionalProperties: false },
);

const CreateTaskSchema = Type.Object(
  {
    projectId: Type.String({ minLength: 1 }),
    title: Type.String({ minLength: 1 }),
    spec: Type.String({ minLength: 1 }),
    acceptanceCriteria: Type.Array(Type.String({ minLength: 1 }), { minItems: 1 }),
    steps: Type.Array(StepInputSchema, { minItems: 1 }),
  },
  { additionalProperties: false },
#
);

export default definePluginEntry({
  id: "project-orchestrator",
  name: "Project Orchestrator",
  description: "Task/project orchestration tools with durable Task Flow state",

  register(api) {
    api.registerTool(
      (ctx) => ({
        name: "create_task",
        description: "Create a task, write canonical task/step files, and open a managed Task Flow",

        parameters: CreateTaskSchema,
#
        async execute(_toolCallId, params) {
          if (!ctx.workspaceDir) {
            throw new Error("create_task requires a workspaceDir.");
          }

          const taskFlow = api.runtime.taskFlow.fromToolContext(ctx);
          const now = Date.now();

          const taskId = `${slugify(params.title)}-${randomUUID().slice(0, 8)}`;
          const taskDir = path.join(
            ctx.workspaceDir,
            "projects",
            params.projectId,
            "tasks",
            taskId,
#
          );
          const stepsDir = path.join(taskDir, "steps");
          const taskJsonPath = path.join(taskDir, "task.json");

          const steps = params.steps.map((step, index) => {
            const stepId = step.id?.trim() || `step-${String(index + 1).padStart(2, "0")}`;
            return {
              id: stepId,
              index: index + 1,
              title: step.title,
              goal: step.goal,
              passCriteria: step.passCriteria,
              status: "queued" as const,
              file: `steps/${stepId}.json`,
            };
#
          });

          // Create the durable managed flow first.
          const flow = taskFlow.createManaged({
            controllerId: "project-orchestrator/task",
            goal: `Execute task ${taskId}: ${params.title}`,
            currentStep: "await-start",
            stateJson: {
              schemaVersion: 1,
              projectId: params.projectId,
              taskId,
              title: params.title,
              status: "draft",
              activeStepId: steps[0]?.id ?? null,
              taskPath: "task.json",
#
              stepCount: steps.length,
            },
            waitJson: {
              kind: "task-await-start",
              taskId,
              nextStepId: steps[0]?.id ?? null,
            },
          });

          const taskDoc = {
            schemaVersion: 1,
            id: taskId,
            projectId: params.projectId,
            title: params.title,
            spec: params.spec,
#
            acceptanceCriteria: params.acceptanceCriteria,
            status: "draft",
            flowId: flow.flowId,
            flowRevision: flow.revision,
            currentFlowStep: flow.currentStep,
            activeStepId: steps[0]?.id ?? null,
            createdAt: isoNow(now),
            updatedAt: isoNow(now),
            steps: steps.map((step) => ({
              id: step.id,
              index: step.index,
              title: step.title,
              status: step.status,
              file: step.file,
            })),
#
          };

          await mkdir(stepsDir, { recursive: true });
          await writeFile(taskJsonPath, `${JSON.stringify(taskDoc, null, 2)}\n`, "utf8");

          for (const step of steps) {
            const stepDoc = {
              schemaVersion: 1,
              id: step.id,
              taskId,
              projectId: params.projectId,
              index: step.index,
              title: step.title,
              goal: step.goal,
              passCriteria: step.passCriteria,
#
              status: "queued",
              flowId: flow.flowId,
              builder: {
                status: "idle",
                sessionKey: null,
                runId: null,
              },
              qa: {
                status: "idle",
                sessionKey: null,
                runId: null,
              },
              createdAt: isoNow(now),
              updatedAt: isoNow(now),
            };
#

            await writeFile(
              path.join(stepsDir, `${step.id}.json`),
              `${JSON.stringify(stepDoc, null, 2)}\n`,
              "utf8",
            );
          }

          return {
            content: [
              {
                type: "text",
                text: `Created task ${taskId} with ${steps.length} steps and Task Flow ${flow.flowId}.`,
              },
            ],
#
            details: {
              taskId,
              flowId: flow.flowId,
              flowRevision: flow.revision,
              taskPath: taskJsonPath,
              stepFiles: steps.map((s) => s.file),
            },
          };
        },
      }),
      { name: "create_task", optional: true },
    );
  },
});