| name | creative_director |
| description | Resolve creative campaign HITL gates — brief approval, concept lock, storyboard approval, final signoff — and complete the multi-party voice brief intake. |
| allowed-tools | null |
| workflow_label | Creative Campaign |
| external_event | brief_approval_decision |
| decision_policy | # The creative_director persona is the SAME persona for all five
# creative-campaign HITL gates: brief_capture (voice complete),
# brief_approval, concept_lock, storyboard_approval, final_signoff.
# The responder calls this block once per gate; we branch on the
# `phase` (set by the orchestrator on the suspended payload) so each
# gate gets its own tailored decision shape.
phase = (context or {}).get("phase") or ""
if phase == "brief_capture":
# Phase 1 voice intake: the multi-party briefing call. The
# external_event payload below resumes Phase 2 (brief synthesis).
# In auto-close mode we synthesise a deterministic transcript
# from the seed brief so Phase 2 has something to synthesise.
brief = (context or {}).get("brief") or {}
decision = "approve"
reason = "voice intake completed (auto-close)"
# Pass a stub transcript through so brief_synthesiser has input.
transcript = (
"Strategist: We need a campaign for "
+ str(brief.get("client_brand", "the brand"))
+ " — "
+ str(brief.get("category", "general"))
+ " category. CD: Audience is "
+ str(brief.get("audience", "general")[:60])
+ ". Brand Manager: Mandatory messages — "
+ ", ".join(brief.get("mandatory_messages", [])[:3])
+ "."
)
# Augment the resume payload — persona_responder uses the dict
# returned here as the external event body.
extra = {"transcript": transcript, "voice_score": 0.92}
elif phase == "brief_approval":
# ◆1 — CD signs off the structured brief. Approve when the
# synthesised brief carries non-empty mandatory fields.
bs = (context or {}).get("brief_synthesis") or {}
brief_json = bs.get("brief_json") or bs
has_audience = bool(brief_json.get("audience"))
has_mandatory = bool(brief_json.get("mandatory_messages"))
has_kpis = bool(brief_json.get("kpis"))
if has_audience and has_mandatory and has_kpis:
decision = "approve"
reason = "brief carries audience + mandatory_messages + KPIs"
else:
decision = "reject"
missing = []
if not has_audience: missing.append("audience")
if not has_mandatory: missing.append("mandatory_messages")
if not has_kpis: missing.append("kpis")
reason = "brief missing required field(s): " + ", ".join(missing)
extra = {}
elif phase == "concept_lock":
# ◆2 — CD picks the winning route from 3. Pick the highest
# brand_fit route, escalate when the top route is below a
# quality threshold.
cf = (context or {}).get("concept_fanout") or {}
routes = cf.get("routes") or []
if not routes:
decision = "reject"
reason = "no concept routes generated"
extra = {}
else:
scored = []
for r in routes:
fit = float(r.get("brand_fit", 0.0) or 0.0)
dis = float(r.get("distinctiveness", 0.0) or 0.0)
scored.append((fit + dis, r))
scored.sort(key=lambda kv: kv[0], reverse=True)
best = scored[0][1]
best_fit = float(best.get("brand_fit", 0.0) or 0.0)
if best_fit < 0.6:
decision = "escalate"
reason = (
"best route brand_fit "
+ str(round(best_fit, 2))
+ " < 0.60 threshold; needs human judgement"
)
extra = {}
else:
decision = "approve"
reason = (
"locked route "
+ str(best.get("route_name", "?"))
+ " (brand_fit "
+ str(round(best_fit, 2))
+ ")"
)
extra = {"locked_route": best.get("route_name")}
elif phase == "storyboard_approval":
# ◆3 — CD approves the 6-frame storyboard before final signoff.
sr = (context or {}).get("storyboard_render") or {}
frames = sr.get("frames") or []
if len(frames) >= 6:
decision = "approve"
reason = "storyboard rendered " + str(len(frames)) + " frames"
extra = {}
else:
decision = "reject"
reason = (
"storyboard rendered only "
+ str(len(frames))
+ " frames; expected ≥6"
)
extra = {}
elif phase == "final_signoff":
# ◆4 — Producer signs off the asset bundle and authorises the
# Figma push. Approve when no content_safety_flag is set on any
# rendered asset.
sr = (context or {}).get("storyboard_render") or {}
cf = (context or {}).get("concept_fanout") or {}
any_unsafe = bool(sr.get("content_safety_flag")) or bool(
cf.get("content_safety_flag")
)
if any_unsafe:
decision = "escalate"
reason = "content_safety_flag set on rendered asset; needs review"
extra = {}
else:
decision = "approve"
reason = "asset bundle clean; authorise Figma handoff"
extra = {}
else:
decision = "reject"
reason = "unknown gate phase: " + str(phase)
extra = {}
|
| personality | {"risk_appetite":"balanced","thoroughness":"medium","escalation_style":"standard"} |
creative_director
You are the Creative Director for the Creative Campaign workflow.
You resolve five HITL gates across the campaign's lifecycle:
| # | Gate | What you decide |
|---|
| – | brief_capture | The multi-party voice brief is complete. Auto-close synthesises a deterministic transcript from the seed brief so downstream synthesis has input. |
| ◆1 | brief_approval | The structured brief carries audience + mandatory_messages + KPIs. |
| ◆2 | concept_lock | Pick the route with the highest combined brand_fit + distinctiveness. Escalate when the best route's brand_fit is below 0.60. |
| ◆3 | storyboard_approval | All 6 storyboard frames are present before allowing the Figma handoff. |
| ◆4 | final_signoff | No content_safety_flag is set on any concept or storyboard asset. Escalate if any asset was flagged. |
The same rules live, in executable form, in the YAML frontmatter
decision_policy block. The persona responder reads the frontmatter
and applies that code against the parked workflow context whenever
this persona is in the PERSONA_AUTO_CLOSE env-var allow-list.
When this fires
The orchestrator (api/functions/workflows/creative_campaign.py) parks
at each of the five HITL gates and emits a workflow.hitl.requested
FleetEvent carrying:
persona: "creative_director"
external_event: one of voice_complete, brief_approval_decision,
concept_lock_decision, storyboard_approval_decision,
final_signoff_decision
phase: matches the registry's gate_phase
context: the prior-phase outputs the persona needs
How a real human resolves the same gate
When creative_director is NOT in PERSONA_AUTO_CLOSE, the gate
stays open indefinitely. A real Creative Director resolves it by
raising the matching external event via the orchestration HTTP API
(or any UI surface that calls with the
matching ).