UNPKG

@agentled/cli

Version:

CLI for Agentled — manage workflows, apps, and knowledge from the command line. Zero context-window cost for AI agents.

152 lines (115 loc) 4.95 kB
# 05 — Child workflow contracts: composable workflows with typed returns **Problem**: Monolithic workflows become unmaintainable, and child workflows called from orchestrators fail silently because their return contracts aren't defined the calling workflow gets `undefined` for every field it tries to read. **Why it fails silently**: A child workflow that ends with a `milestone` step instead of a `return` step completes successfully from the platform's perspective. The calling workflow receives no data and no error. Every field reference like `{{steps.call-child.score}}` resolves to empty string. --- ## The milestone vs return mistake ```yaml # Wrong: child workflow ends with milestone - id: score-company type: ai-action prompt: "Score this company 0-100..." responseStructure: score: "number" decision: "string" - id: done # ← milestone = terminal, no data returned to caller type: milestone name: "Complete" ``` The calling orchestrator runs `call-workflow` and gets back nothing. `{{steps.call-child.score}}` is empty. No error is raised. --- ## Anti-pattern ```yaml # Wrong: child workflow steps: - id: enrich ... - id: score ... - id: done # milestone doesn't return data type: milestone # Calling orchestrator: - id: call-child type: call-workflow input: { companyUrl: "{{input.url}}" } - id: use-result type: ai-action prompt: "Based on score {{steps.call-child.score}}..." # ^ always empty — milestone returned nothing ``` --- ## Correct pattern Child workflows must end with a `return` step that explicitly declares what they return: ```yaml # Correct: child workflow steps: - id: enrich ... - id: score type: ai-action responseStructure: score: "number 0-100" decision: "invest | pass | monitor" summary: "string" - id: return-results # ← return step, not milestone type: return returnConfig: fields: - name: score # the name the caller uses stepId: score # which step produced it field: score # which field from that step - name: decision stepId: score field: decision - name: summary stepId: score field: summary # Calling orchestrator: - id: call-child type: call-workflow input: { companyUrl: "{{input.url}}" } - id: use-result type: ai-action prompt: "Based on score {{steps.call-child.score}}, decision: {{steps.call-child.decision}}..." # ^ now populated correctly ``` --- ## Designing a return contract A return contract is the interface between the child workflow and its callers. Treat it like a function signature: 1. **Be explicit**: list every field the caller might need don't assume they'll dig into nested objects 2. **Use flat field names**: `score` not `scoringCard.total_score` callers reference these as template variables 3. **Match names to caller expectations**: if the orchestrator uses `{{steps.call-child.decision}}`, the return contract must export a field named `decision` 4. **Version changes carefully**: adding fields is safe; renaming or removing fields breaks every orchestrator that calls this child ```yaml # Comprehensive return contract returnConfig: fields: - { name: score, stepId: score-step, field: total_score } - { name: decision, stepId: score-step, field: decision } - { name: summary, stepId: score-step, field: executive_summary } - { name: teamEvaluations, stepId: eval-team, field: evaluations } - { name: rawData, stepId: enrich, field: companyProfile } ``` --- ## The god-workflow anti-pattern A single workflow with 25+ steps that handles intake, enrichment, scoring, routing, outreach, and CRM sync: ``` trigger fetch enrich score route draft-email approve send update-crm update-kg notify-slack generate-report archive done ``` Problems: - A failure in step 12 requires re-running steps 1-11 - You can't reuse the scoring logic in another context - Testing requires running the entire pipeline end-to-end - A single developer change can break the entire flow **Break it into composable child workflows:** ``` Orchestrator: trigger call: enrich-workflow call: score-workflow call: route-workflow done enrich-workflow: fetch enrich return { profile } score-workflow: receive profile score return { score, decision } route-workflow: receive decision route draft approve send return { sent } ``` Each child workflow can be tested independently, retried independently, and reused by other orchestrators. --- ## One-line rule > Child workflows must end with a `return` step (not `milestone`) with an explicit `returnConfig.fields` list milestone completes silently with no data returned to the caller.