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.
Step 1: Create the agent
Section titled “Step 1: Create the agent”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.
Step 2: Design the workflow
Section titled “Step 2: Design the workflow”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:
start→check_turn_count(condition: has the candidate answered enough questions yet?)- 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. - If true (enough questions asked) →
close_interview(llm, drafts a professional closing message) →respond_close(response) →end_session(withrunEvaluation: 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_countis aconditionnode, not arouter. This is a genuine two-way check (“has enough happened yet, yes or no”), which is exactly whatconditionis for. Its two edges are labeled exactly"true"and"false", matchingcondition’s hard requirement described in Condition Node — a mislabeled edge here stops the whole interview at that node with a clear error.turnCount >= 4is validexpr-evalsyntax —expr-evalis what powerscondition, and it is not JavaScript.>=is shared with JS, but==,and,or,notare the operators you’d need for anything more complex here — see Condition Node if you extend this expression.turnCountis a reserved, built-in context variable (see the LLM Node config reference) — you don’t need to write anything to populate it yourself.ask_questionandclose_interviewboth setstreamOutput: false. Their output goes intorespond_question/respond_close’smessageTemplaterather than being sent directly — see the LLM Node gotcha thatstreamOutputdefaults totrueand needs to be explicitly turned off for a node whose output isn’t itself session-facing in a streaming sense.respond_questionhas no outgoing edge. That’s intentional, not an oversight — publishing only requires every node be reachable fromstart, not that every node have an outgoint path onward. Aresponsenode with no further edge is how a turn ends while leaving the session open for the candidate’s next message; only reachingend_sessionactually terminates the session.- Adjust
turnCount >= 4to 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.
Step 3: Publish and attach
Section titled “Step 3: Publish and attach”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.
Getting the result into an ATS
Section titled “Getting the result into an ATS”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.
Next steps
Section titled “Next steps”- Condition Node — full
expr-evalsyntax reference for extendingcheck_turn_count. - End Session Node — the
runEvaluationandrequireConfirmationflags in full. - Evaluation & Export — the complete
EvaluationDtoshape and polling pattern. - Agents — the
AgentUseCaseenum and how it changes evaluation shape. - Workflow Best Practices — general templating and reliability guidance applicable to the
ask_question/close_interviewprompts above.