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 reviewPrepare 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.
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 studioA 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.
| Decision | Result |
|---|---|
| Accept either draft | accepted, with the reviewed draft |
| Stop either draft | declined |
| Request changes to both drafts | revision-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.