Skip to content

AI Agent Loop

Generates content with an AI model, evaluates the output against a quality gate, and retries with a refined prompt if the quality threshold is not met.

Workflow structure

Nodes

#Node nameTypePurpose
1Parse RequestStepExtract topic and requirements from the trigger
2Generation LoopLoop (times: 3)Retry generation up to 3 times
3Generate ContentHTTP RequestCall OpenAI Chat Completions
4Evaluate QualityStepScore the generated content
5Quality GateBranchPass if score ≥ 80, else continue loop
6BreakBreakExit loop early on success
7Save ResultHTTP RequestPOST the accepted content to your API

Trigger

API trigger.

bash
curl -X POST https://app.awaitstep.dev/api/workflows/<id>/trigger \
  -H "Authorization: Bearer ask_yourkey" \
  -H "Content-Type: application/json" \
  -d '{
    "connectionId": "<conn-id>",
    "params": {
      "topic": "Benefits of serverless workflows",
      "tone": "professional",
      "wordCount": 200
    }
  }'

Generated TypeScript

typescript
import { WorkflowEntrypoint, WorkflowEvent, WorkflowStep } from 'cloudflare:workers'

export class AiAgentWorkflow extends WorkflowEntrypoint<Env, Params> {
  async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
    const parse_request = await step.do('Parse Request', async () => {
      const { topic, tone, wordCount } = event.payload ?? {}
      if (!topic) throw new Error('topic is required')
      return {
        topic: String(topic),
        tone: String(tone ?? 'professional'),
        wordCount: Number(wordCount ?? 200),
      }
    })

    const generation_loop = await step.do('Generation Loop', async () => {
      let _output
      for (let _loop_i = 0; _loop_i < 3; _loop_i++) {
        const generate_content = await step.do(`Generate Content [${_loop_i}]`, async () => {
          const prompt =
            _loop_i === 0
              ? `Write a ${parse_request.wordCount}-word ${parse_request.tone} article about: ${parse_request.topic}`
              : `Rewrite the following article about "${parse_request.topic}" to improve clarity and engagement. Keep it ${parse_request.wordCount} words.`

          const res = await fetch('https://api.openai.com/v1/chat/completions', {
            method: 'POST',
            headers: {
              Authorization: `Bearer ${env.OPENAI_API_KEY}`,
              'Content-Type': 'application/json',
            },
            body: JSON.stringify({
              model: 'gpt-4o-mini',
              messages: [{ role: 'user', content: prompt }],
              max_tokens: 500,
            }),
          })
          if (!res.ok) throw new Error(`OpenAI error: ${res.status}`)
          const data = await res.json()
          return { content: data.choices[0]?.message?.content ?? '' }
        })

        const evaluate_quality = await step.do(`Evaluate Quality [${_loop_i}]`, async () => {
          const wordCount = generate_content.content.split(/\s+/).length
          const hasIntro = generate_content.content.length > 100
          const meetsLength =
            Math.abs(wordCount - parse_request.wordCount) <= parse_request.wordCount * 0.2
          const score = (hasIntro ? 50 : 0) + (meetsLength ? 50 : 20)
          return { score, wordCount }
        })

        if (evaluate_quality.score >= 80) {
          _output = {
            content: generate_content.content,
            score: evaluate_quality.score,
            attempts: _loop_i + 1,
          }
          break
        }
      }
      return _output
    })

    await step.do('Save Result', async () => {
      const res = await fetch(`${env.API_BASE_URL}/content`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          topic: parse_request.topic,
          content: generation_loop?.content,
          score: generation_loop?.score,
          attempts: generation_loop?.attempts,
        }),
      })
      if (!res.ok) throw new Error(`API error: ${res.status}`)
    })
  }
}

Required env vars

VariableDescription
OPENAI_API_KEYOpenAI API key
API_BASE_URLBase URL of your backend API

Customising the quality gate

The example uses a simple word-count and length heuristic. Replace the Evaluate Quality step with a call to a second AI model (e.g. an OpenAI moderation or scoring prompt) for production use cases.

Extending the loop

  • Increase the loop count to allow more retries.
  • Add a Sleep node inside the loop body to introduce a delay between attempts.
  • Persist each attempt's content to a database so you can review all generated versions.