← All examples
ContentAIFork

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 lane

The 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.woml

Replace 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.