flowmarkup:
  title: Rate Limited AI Processor
  version: 1
  documentation: |
    Demonstrates flow-level and action-level rate limiting.

    Flow-level rate-limit throttles how often the flow itself can be invoked
    across all engine instances (GLOBAL scope). Action-level rate-limit
    throttles individual steps — with per-tenant bucketing via key:.
  requires:
    SERVICES: [claude, gemini]
    CONTEXT: { read: [tenant_id] }

  # Flow-level defaults — all action steps inherit these unless overridden
  defaults:
    retry: =STD_RETRY
    timeout: 30s

  # Flow-level: at most 100 invocations per minute across all engine instances.
  # If the window is full, WAIT up to 30s for a slot (then throw RateLimitError).
  rateLimit:
    invocations: 100
    per: 1m
    scope: GLOBAL
    strategy: WAIT
    timeout: 30s

  # Shared retry policy — referenced via CEL in defaults: and individual steps
  const:
    STD_RETRY: {maxAttempts: 3, delay: 2s, backoff: EXPONENTIAL}

  input:
    prompt_text: TEXT
  output:
    result: TEXT
  examples:
    basic_query:
      _notes_: Process a simple prompt with both AI models
      input:
        prompt_text: What is the capital of France?
      output:
        result: Paris is the capital of France. | The capital of France is Paris.
    code_generation:
      _notes_: Code generation prompt across two models
      input:
        prompt_text: Write a hello world in Go
      output:
        result: "func main() { fmt.Println(\"Hello, World!\") } | package main\nimport \"fmt\"\nfunc main() { fmt.Println(\"Hello, World!\") }"

  vars:
    claude_result: ""
    gemini_result: ""
    combined: ""

  do:
    # Action-level: dynamic limit from global config — operator-tunable without redeployment.
    # Per-tenant counter via key:; WAIT up to 2m for a slot.
  - call:
      _label_: Fetch Claude response
      service: =SERVICES.claude
      operation: generate
      rateLimit:
        invocations: =GLOBAL.claude_rate_cap
        per: =GLOBAL.claude_rate_window      # dynamic — driven from config
        scope: GLOBAL
        key: =CONTEXT.tenant_id
        strategy: WAIT
        timeout: 2m
      params:
        prompt: =prompt_text
      result:
        claude_result: =RESULT.text

    # Action-level: strict per-tenant cap on a metered external API.
    # REJECT immediately when the hourly budget is exhausted — no queuing.
  - call:
      _label_: Fetch Gemini response
      service: =SERVICES.gemini
      operation: generate
      rateLimit:
        invocations: 10
        per: 1h
        scope: GLOBAL
        key: =CONTEXT.tenant_id
        strategy: REJECT
      params:
        prompt: =prompt_text
      result:
        gemini_result: =RESULT.text

  - set:
      combined: "=claude_result + ' | ' + gemini_result"

  - return: {result: =combined}
  catch:
    RateLimitError:
    - logWarn: "Rate limit exceeded for tenant {{CONTEXT.tenant_id}}: {{ERROR.MESSAGE}}"
    - throw:
        error: RateLimitError
        message: Service temporarily unavailable — please retry later
