LaufwerkLaufwerk
Learn Laufwerk

A complete research-and-draft workflow

Combine agents, structured handoffs, human feedback and a bounded revision policy in one runnable file.

This workflow turns supplied notes into a short draft, asks a person to review it and permits one revision. It returns an explicit outcome: accepted, declined or revision limit reached. It saves no document and publishes nothing.

Topic + notes → research → draft 1 → human review → accept / decline

                                          revise → feedback → draft 2 → final review

Prepare the project

Start with getting started and Codex authentication. The example uses local execution with host-user permissions. It asks agents to use only supplied notes and not touch files; these are instructions, not enforced isolation. Use the Docker execution pattern if your task requires isolation.

Save this complete module at laufwerk/workflows/research-and-draft/workflow.ts.

workflow.ts
import { Workflow } from "@effect/workflow";
import { Effect, Schema } from "effect";
import { Human, Session } from "@laufwerk/sdk";
import { createLocalExecution } from "@laufwerk/execution";
import { createCodex } from "@laufwerk/sdk/harness/codex";

const execution = createLocalExecution({ credentials: "codex-subscription" });
const researcher = execution.agent({
  id: "researcher", harness: createCodex(),
  instructions: "Work only from the supplied notes. Extract relevant facts and uncertainty. Do not use tools or modify files.",
});
const writer = execution.agent({
  id: "writer", harness: createCodex(),
  instructions: "Write a short draft from the supplied brief. Preserve uncertainty. Do not use tools or modify files.",
});
const Brief = Schema.Struct({
  facts: Schema.Array(Schema.String),
  uncertainty: Schema.Array(Schema.String),
});
const Draft = Schema.Struct({ text: Schema.String });
const Result = Schema.Struct({
  status: Schema.Literal("accepted", "declined", "revision-limit"),
  draft: Schema.String,
  rounds: Schema.Number,
  feedback: Schema.String,
});
export const workflow = Workflow.make({
  name: "research-and-draft",
  payload: { requestId: Schema.String, topic: Schema.String, notes: Schema.String },
  success: Result, error: Schema.String,
  idempotencyKey: input => input.requestId,
});
export const layer = workflow.toLayer(input => Effect.gen(function* () {
  const brief = yield* Session.run({
    key: "research", agent: researcher, output: Brief,
    prompt: JSON.stringify({ topic: input.topic, notes: input.notes }),
  });
  let previousDraft = "";
  let feedback = "";
  for (let round = 0; round < 2; round++) {
    const draft = yield* Session.run({
      key: `draft-${round}`, agent: writer, output: Draft,
      prompt: JSON.stringify({ topic: input.topic, brief, previousDraft, feedback }),
    });
    if (!draft.text.trim()) return yield* Effect.fail("Writer returned an empty draft");
    const decision = yield* Human.select({
      key: `review-${round}`, title: `Review draft ${round + 1}`,
      description: draft.text,
      options: [
        { value: "accept", label: "Accept" },
        { value: "revise", label: "Request changes" },
        { value: "decline", label: "Stop" },
      ],
    });
    if (decision !== "revise") return {
      status: decision === "accept" ? "accepted" as const : "declined" as const,
      draft: draft.text, rounds: round + 1, feedback,
    };
    feedback = yield* Human.text({
      key: `feedback-${round}`, title: "What should change?",
    });
    previousDraft = draft.text;
  }
  return { status: "revision-limit" as const, draft: previousDraft, rounds: 2, feedback };
}).pipe(Effect.mapError(String)));

Run it and review the draft

bunx laufwerk@0.0.1-alpha.13 run research-and-draft --input '{"requestId":"launch-1","topic":"Pilot announcement","notes":"The pilot starts Monday. Three teams are participating. Pricing has not been decided."}' --json
bunx laufwerk@0.0.1-alpha.13 studio

A model turn can take time. The CLI may return a waiting run once the first review is registered. Open that run in Studio. Request changes to try the second draft, then accept, decline or request further changes.

DecisionResult
Accept either draftaccepted, with the reviewed draft
Stop either draftdeclined
Request changes to both draftsrevision-limit, with the second draft and unresolved feedback

Use status RUN_ID --json to inspect the stored result. A completed run with revision-limit did not achieve acceptance. Use a new request ID for a new attempt.

Why the boundaries are here

  • Research and writing use separate one-shot sessions. Each manages its own lifecycle; this example has no continuous-session cleanup to implement.
  • The second writer receives the previous draft and feedback explicitly. It does not inherit the first writer's conversation.
  • Schemas make handoffs usable; the nonempty check catches one invalid candidate. The human still needs to assess whether the text is supported by the notes.
  • Each review has a stable round-specific key, so replay reuses the right answer.
  • A failed provider turn fails the workflow. It is not counted as negative feedback.

Extend one boundary at a time

Use a custom review form to return edited text and comments together. Use workspace sessions when the deliverable is a file change. Add an external publication activity only after defining its permissions, idempotency and recovery receipt. Compare changes on fixed cases before automating more of the process.


← Applications, HTTP and ownership · API reference →

On this page