跳转到内容

Recipe: First-Round Candidate Screening Agent

此内容尚不支持你的语言。

This recipe builds a first-round technical screening agent: it asks a candidate a fixed number of interview questions one at a time, gives brief feedback after each answer, closes the conversation professionally once it’s asked enough, and produces a structured evaluation your own systems can pull afterward.

This is one of the more mature use-case patterns built on Wetel’s primitives — it combines the INTERVIEW use case (which drives a purpose-built evaluation shape), a looping condition node keyed on the built-in turnCount context variable, and the end_session node’s evaluation flag.

Every request below needs the x-huat-platform: customer header and a valid dashboard Authorization: Bearer <token> — see Getting Started and Authentication if you haven’t set those up.

mutation CreateInterviewAgent {
createAgent(
input: {
name: "Backend Engineer Screener"
position: "Technical Recruiter"
personaPrompt: "You are an experienced technical recruiter conducting a first-round screening interview for a backend engineering role. Ask ONE technical question at a time. After the candidate answers, give brief, constructive feedback before moving on — never a paragraph, one or two sentences. Stay professional, warm, and concise throughout."
useCase: INTERVIEW
voiceTier: STANDARD
}
) {
id
}
}

useCase: INTERVIEW matters beyond record-keeping — it selects the evaluation prompt and recommendation shape that runs automatically at session end (see Step 4). personaPrompt defines the interviewer’s identity and tone; it deliberately says nothing about how many questions to ask or when to stop — that logic lives in the workflow graph, not the prompt, per the Agents guidance to keep step-by-step behavior out of the persona. Assume this returned agent id 74.

The graph re-evaluates from start on every incoming candidate message, and turnCount — a built-in context variable incremented automatically as the conversation progresses — is what decides whether to ask another question or move to closing. This is the same looping pattern described for router nodes in Workflows Overview: a branch that routes back to an earlier point in the conversation is a fully supported design, not something publishing rejects.

Flow, on every turn:

  1. startcheck_turn_count (condition: has the candidate answered enough questions yet?)
  2. If false (not enough questions yet) → ask_question (llm, asks the next question and gives brief feedback on the previous answer) → respond_question (response) — this branch has no further outgoing edge, so the turn ends here and the workflow waits for the candidate’s next message.
  3. If true (enough questions asked) → close_interview (llm, drafts a professional closing message) → respond_close (response) → end_session (with runEvaluation: true), which actually ends the session.
mutation CreateWorkflow($input: CreateWorkflowInput!) {
createWorkflow(input: $input) {
id
}
}
{
"input": {
"name": "Backend Engineer Screening Interview",
"description": "Asks up to 4 technical questions, then closes professionally and triggers evaluation."
}
}

Assume this returned workflow id 55.

mutation UpdateWorkflow($input: UpdateWorkflowInput!) {
updateWorkflow(input: $input) {
id
version
}
}
{
"input": {
"id": 55,
"nodes": [
{
"id": "start",
"type": "start",
"label": "Start",
"config": {},
"position": { "x": 0, "y": 0 }
},
{
"id": "check_turn_count",
"type": "condition",
"label": "Enough Questions Asked?",
"config": { "expression": "turnCount >= 4" },
"position": { "x": 0, "y": 100 }
},
{
"id": "ask_question",
"type": "llm",
"label": "Ask Next Question",
"config": {
"outputVar": "interviewerTurn",
"promptTemplate": "Conversation so far: {{conversationHistory}}. Give brief, constructive feedback on the candidate's most recent answer if there was one, then ask exactly one new technical backend-engineering interview question. Do not ask more than one question.",
"streamOutput": false
},
"position": { "x": -200, "y": 200 }
},
{
"id": "respond_question",
"type": "response",
"label": "Send Question",
"config": {
"messageTemplate": "{{interviewerTurn}}",
"mood": "neutral"
},
"position": { "x": -200, "y": 300 }
},
{
"id": "close_interview",
"type": "llm",
"label": "Draft Closing Message",
"config": {
"outputVar": "closingMessage",
"promptTemplate": "Conversation so far: {{conversationHistory}}. Thank the candidate for their time, briefly and warmly, and let them know next steps will follow by email. Do not reveal a hire/no-hire decision.",
"streamOutput": false
},
"position": { "x": 200, "y": 200 }
},
{
"id": "respond_close",
"type": "response",
"label": "Send Closing Message",
"config": {
"messageTemplate": "{{closingMessage}}",
"mood": "helpful"
},
"position": { "x": 200, "y": 300 }
},
{
"id": "end",
"type": "end_session",
"label": "End Interview",
"config": { "runEvaluation": true },
"position": { "x": 200, "y": 400 }
}
],
"edges": [
{ "id": "e1", "source": "start", "target": "check_turn_count" },
{
"id": "e2",
"source": "check_turn_count",
"target": "ask_question",
"label": "false"
},
{
"id": "e3",
"source": "check_turn_count",
"target": "close_interview",
"label": "true"
},
{ "id": "e4", "source": "ask_question", "target": "respond_question" },
{ "id": "e5", "source": "close_interview", "target": "respond_close" },
{ "id": "e6", "source": "respond_close", "target": "end" }
]
}
}

A few deliberate choices worth calling out:

  • check_turn_count is a condition node, not a router. This is a genuine two-way check (“has enough happened yet, yes or no”), which is exactly what condition is for. Its two edges are labeled exactly "true" and "false", matching condition’s hard requirement described in Condition Node — a mislabeled edge here stops the whole interview at that node with a clear error.
  • turnCount >= 4 is valid expr-eval syntax — expr-eval is what powers condition, and it is not JavaScript. >= is shared with JS, but ==, and, or, not are the operators you’d need for anything more complex here — see Condition Node if you extend this expression. turnCount is a reserved, built-in context variable (see the LLM Node config reference) — you don’t need to write anything to populate it yourself.
  • ask_question and close_interview both set streamOutput: false. Their output goes into respond_question/respond_close’s messageTemplate rather than being sent directly — see the LLM Node gotcha that streamOutput defaults to true and needs to be explicitly turned off for a node whose output isn’t itself session-facing in a streaming sense.
  • respond_question has no outgoing edge. That’s intentional, not an oversight — publishing only requires every node be reachable from start, not that every node have an outgoint path onward. A response node with no further edge is how a turn ends while leaving the session open for the candidate’s next message; only reaching end_session actually terminates the session.
  • Adjust turnCount >= 4 to whatever number of questions fits your actual screening depth — interview turns (Condition: turnCount ≥ 3) and “3–4 questions” are both reasonable starting points; this recipe uses 4 as a concrete example.
mutation PublishWorkflow($id: Int!) {
publishWorkflow(id: $id) {
id
isPublished
version
}
}
{ "id": 55 }
mutation AttachWorkflow {
updateAgent(id: "74", input: { workflowId: 55 }) {
id
workflowId
}
}

As with every workflow, isPublished must be true before any session against agent 74 will run it at all — an unpublished workflow executes zero nodes, with no error. See Agents for the full explanation of this trap.

Step 4: Retrieve the structured evaluation

Section titled “Step 4: Retrieve the structured evaluation”

Because end_session’s runEvaluation: true fired when the interview closed, a background job generates a structured evaluation using the shape useCase: INTERVIEW selects — score, a recommendation, strengths, and weaknesses tailored to interview screening rather than a generic conversation summary. This is generated asynchronously after the session ends, so query it a few seconds after the interview closes, not immediately:

query GetScreeningResult($sessionId: Int!) {
evaluation(sessionId: $sessionId) {
score
recommendation
summary
strengths
weaknesses
generatedAt
}
}

generatedAt is null until the job finishes — see Evaluation & Export for the full field reference and the request-then-poll pattern this query follows.

Wetel doesn’t yet push events to a URL you register — Outbound Webhooks is not shipped as of this writing. The working pattern today is the reverse: your own backend decides when a screening session has ended (for example, by tracking session state on your side, or polling), calls the evaluation query above, and forwards the result to your ATS (Greenhouse, Workday, Lever, or anything else with an inbound API) using whatever HTTP client your backend already has. That forwarding step happens entirely in your own systems — it isn’t a Wetel workflow node, since the evaluation itself doesn’t exist yet at any point during the workflow’s own execution.

  • Condition Node — full expr-eval syntax reference for extending check_turn_count.
  • End Session Node — the runEvaluation and requireConfirmation flags in full.
  • Evaluation & Export — the complete EvaluationDto shape and polling pattern.
  • Agents — the AgentUseCase enum and how it changes evaluation shape.
  • Workflow Best Practices — general templating and reliability guidance applicable to the ask_question/close_interview prompts above.