Our system can now classify a topic, conduct research, back it with examples, identify weak assumptions and unsupported claims, and structure everything into a content brief before handing it over to the writing agents.
Before the writing agents generate a full post, we need an opening that's compelling enough to stop people from scrolling.
That's why we built the Hook Agent.
Unlike the previous agents, it doesn't consume the topic, research, examples or critique directly. Instead, it only takes the ContentBrief generated by the Brief Agent.
Content Brief
│
▼
Hook Agent
│
▼
5 Hook Variations
Most of us aren't satisfied with the first hook we get. Instead of generating just one opening, I generate five different hook variations.
The Draft Agent can later use any one of these to generate complete posts. One hook format doesn't work for all posts, so we use different types of hook variations like Bold Claim, Statistic, Confession, Question, One-liner.
Before we start writing a single line of code, let's start with the system prompt. I explicitly instruct it to generate exactly five hooks, define the hook formats with examples, enforce a fixed ordering, and return valid JSON.
SYSTEM_PROMPT = """
Generate exactly 5 hooks.
Formats:
- Bold claim
- Stat
- Confession
- Question
- One-liner
Return valid JSON.
"""
The production prompt is much longer, but I've shortened it here to highlight the core rules.
So the JSON response returns hooks in the exact order which makes them easy to compare and lets downstream agents reference them deterministically. For example, hooks[0] is always the bold claim.
0 -> Bold Claim
1 -> Stat
2 -> Confession
3 -> Question
4 -> One-liner
Instead of manually constructing the prompt, I convert the ContentBrief object into a dictionary using model_dump() and serialize it into a structured user message.
brief_dict = brief.model_dump()
user_message = "\n".join(
f"{k}: {v}"
for k, v in brief_dict.items()
)
Just like the Critic Agent, the Hook Agent retries generation up to three times if the model returns malformed JSON or fewer than five hooks.
for attempt in range(max_retries):
Before returning the response, I validate that exactly five hooks were generated. I'd rather return no hooks than fewer than five. The Draft Agent expects all five hook formats, so partial output is treated as a failure.
if len(parsed) == 5:
return parsed
Before writing code, let's breakdown what happens in the function. It takes a ContentBrief object as input, converts it into a dictionary using model_dump(), builds the user message, calls the LLM, strips markdown fences, parses the JSON response, and validates that exactly five hooks were returned.
def generate_hooks(brief: ContentBrief, max_retries = 3) -> list[str]:
brief_dict = brief.model_dump()
user_message = "\n".join([f"{key}:{value}" for key, value in brief_dict.items()])
for attempt in range(max_retries):
raw_response = call_llm(SYSTEM_PROMPT, user_message)
refined = strip_json_fences(raw_response)
try:
parsed = json.loads(refined)
if len(parsed) == 5:
return parsed
except json.JSONDecodeError:
print(f"Hook attempt {attempt + 1} failed, retrying...")
return []
At this point, our system can understand a topic, gather supporting research, find real-world examples, critique them, structure everything into a content brief, and generate five structured hook variations before writing a single post.
In the next article, we'll build the Draft Agent, which takes one of these hooks and turns it into complete LinkedIn post drafts for the user to choose from.
Github Repo: https://github.com/Manav-N4/linkedin-agent#linkedin-agent