Research, approve, and distribute content from one workflow
Generate a researched draft, pause for editorial approval, then run independent multi-step publishing lanes with a selective join.
Content production is rarely one model call. The durable process includes research, generation, review, channel-specific formatting, publishing, archiving, and an operational result.
The workflow at a glance
Text
Weekday schedule
→ Research topic
→ Generate canonical draft
→ Editorial approval
→ Approved? ─ no → Record rejection
└ yes → Fork
newsletter: format → publish ─┐
LinkedIn: format → publish ──┼→ Main result
archive: store source ────────┘ independent laneThe content module
Save as content-studio.ts:
TypeScript
export async function generate(prompt: string, apiKey: string) {
const response = await services.http.request({
method: "POST",
url: "https://api.openai.com/v1/chat/completions",
headers: { authorization: `Bearer ${apiKey}` },
json: {
model: "gpt-4.1-mini",
messages: [{ role: "user", content: prompt }]
},
timeout: "45s"
}, { name: "generate-editorial-content" });
return response.data.choices[0].message.content;
}
export async function publish(
url: string,
content: string,
token: string,
) {
const response = await services.http.request({
method: "POST",
url,
headers: { authorization: `Bearer ${token}` },
json: { content },
timeout: "30s"
}, { name: "publish-editorial-content" });
return response.data;
}The workflow
WOML
<woml>
<imports>
<module name="contentStudio" from="./content-studio.ts" />
</imports>
<workflow
id="editorial-content-pipeline"
name="Editorial content pipeline"
description="Research, review, publish, and archive weekday content."
version="1.0.0"
>
<config concurrency="1" timeout="48h" queue="editorial" />
<triggers>
<schedule id="weekdayEdition" cron="0 8 * * 1-5" timezone="UTC" on-missed="skip" />
<manual id="draftNow" />
</triggers>
<steps>
<step id="research" name="Research today's topic" retry="3">
<script>
const day = new Date().toISOString().slice(0, 10);
const research = await services.contentStudio.generate(
`Research one important workflow automation topic for ${day}. Return evidence, counterpoints, and source URLs.`,
secrets.OPENAI_API_KEY
);
return { day, research };
</script>
</step>
<step id="draft" name="Create canonical draft" retry="3">
<script>
const article = await services.contentStudio.generate(
`Write a practical article from this research:\n\n${context.steps.research.research}`,
secrets.OPENAI_API_KEY
);
return { article };
</script>
</step>
<approval id="editorialReview" name="Review generated article" timeout="24h" on-timeout="reject">
<notify>
<slack
channels="#editorial-review"
bot-token="{{secrets.SLACK_BOT_TOKEN}}"
app-token="{{secrets.SLACK_APP_TOKEN}}"
/>
</notify>
<when-approved><step id="approvedDraft"><script>return { approved: true };</script></step></when-approved>
<when-rejected><step id="rejectedDraft"><script>return { approved: false };</script></step></when-rejected>
</approval>
<step id="publicationPolicy"><script>return { publish: context.steps.editorialReview.decision === "approved" };</script></step>
<choose id="distribution" name="Distribute approved content">
<when test="{{context.steps.publicationPolicy.publish}}">
<fork id="channels" join="newsletter linkedin">
<branch id="newsletter">
<step id="formatNewsletter">
<script>
const content = await services.contentStudio.generate(
`Format this as an email newsletter:\n\n${context.steps.draft.article}`,
secrets.OPENAI_API_KEY
);
return { content };
</script>
</step>
<step id="publishNewsletter" retry="3">
<script>
return services.contentStudio.publish(
"https://newsletter.example.com/issues",
context.steps.formatNewsletter.content,
secrets.NEWSLETTER_API_TOKEN
);
</script>
</step>
</branch>
<branch id="linkedin">
<step id="formatLinkedIn">
<script>
const content = await services.contentStudio.generate(
`Turn this article into a concise LinkedIn post:\n\n${context.steps.draft.article}`,
secrets.OPENAI_API_KEY
);
return { content };
</script>
</step>
<step id="publishLinkedIn" retry="3">
<script>
return services.contentStudio.publish(
"https://social.example.com/linkedin/posts",
context.steps.formatLinkedIn.content,
secrets.SOCIAL_API_TOKEN
);
</script>
</step>
</branch>
<branch id="archive">
<step id="archiveDraft">
<script>
return services.storage.put({
key: `editorial/${context.steps.research.day}.json`,
value: {
research: context.steps.research.research,
article: context.steps.draft.article
},
contentType: "application/json"
});
</script>
</step>
</branch>
</fork>
<step id="publishedResult">
<script>
return {
status: "published",
newsletter: context.steps.publishNewsletter,
linkedin: context.steps.publishLinkedIn
};
</script>
</step>
<result value="{{context.steps.publishedResult}}" />
</when>
<otherwise>
<step id="notPublished"><script>return { status: "rejected" };</script></step>
<result value="{{context.steps.notPublished}}" />
</otherwise>
</choose>
</steps>
</workflow>
</woml>Configure and run it
Terminal
woml secrets set OPENAI_API_KEY
woml secrets set NEWSLETTER_API_TOKEN
woml secrets set SOCIAL_API_TOKEN
woml secrets set SLACK_BOT_TOKEN
woml secrets set SLACK_APP_TOKEN
woml check editorial-content-pipeline.woml
woml run editorial-content-pipeline.womlReplace the publishing endpoints with your newsletter and social APIs.
Why this shows WOML's range
The source reads like an editorial system: one canonical draft, one durable decision, and independent channel lanes. Each lane can contain real code and several steps, the main route waits only for required channels, and the source archive remains owned even though it is not part of the main result.