← Back to cookbook

Job Application Screener

AI evaluates job applications for fit, routes top candidates to human review, and sends rejections.

aiswitchemail

Source

/**
 * Screens job applications. AI evaluates fit, human reviews top candidates.
 */

form job_application {
  label: "Job Application"
  visibility: public
  schema: @json {
    {
      "type": "object",
      "required": ["name", "email", "role", "resume_text"],
      "properties": {
        "name": { "type": "string", "title": "Full Name" },
        "email": { "type": "string", "format": "email", "title": "Email" },
        "role": { "type": "string", "title": "Role Applied For" },
        "resume_text": { "type": "string", "title": "Resume (paste text)" },
        "linkedin_url": { "type": "string", "title": "LinkedIn URL" }
      }
    }
  }
}

workflow screen_application {
  label: "Screen Application"

  root {
    type: code
    label: "Extract application"
    code: @ts { return context.nodes.root.input }
    outputSchema: @json {
      {
        "type": "object",
        "properties": {
          "name": { "type": "string" },
          "email": { "type": "string" },
          "role": { "type": "string" },
          "resume_text": { "type": "string" },
          "linkedin_url": { "type": "string" }
        }
      }
    }
  }

  node evaluate {
    type: ai
    label: "Evaluate candidate"
    kind: object
    model: "google/gemini-2.5-flash"
    prompt: @ts {
      const app = context.nodes.root.output
      return "Evaluate this candidate for the " + app.role + " role. Score their fit.\n\nName: " + app.name + "\n\nResume:\n" + app.resume_text
    }
    schema: @json {
      {
        "type": "object",
        "required": ["score", "strengths", "concerns", "recommendation"],
        "properties": {
          "score": { "type": "number", "minimum": 0, "maximum": 100 },
          "strengths": { "type": "array", "items": { "type": "string" } },
          "concerns": { "type": "array", "items": { "type": "string" } },
          "recommendation": { "type": "string", "enum": ["advance", "maybe", "pass"] }
        }
      }
    }
  }

  node route {
    type: switch
    label: "Route by recommendation"
    cases: ["advance", "maybe", "pass"]
    router: @ts {
      return context.nodes.evaluate.output.recommendation
    }
  }

  node prepare_review {
    type: code
    label: "Human review"
    code: @ts { return {} }
    review: {
      enabled: true
      title: "Review candidate"
      description: "Decide whether to advance this candidate to interview. See the evaluate node output for the AI screening summary. Submit your decision; rejected candidates are notified automatically."
      schema: @json {
        {
          "type": "object",
          "required": ["decision"],
          "properties": {
            "decision": { "type": "string", "enum": ["interview", "reject"], "title": "Decision" },
            "notes": { "type": "string", "title": "Notes" }
          }
        }
      }
      actions: [
        { id: "submit", label: "Submit Decision", outcome: "approve" }
      ]
    }
  }

  node review_decision {
    type: switch
    label: "Route on review decision"
    cases: ["interview", "reject"]
    router: @ts {
      const review = context.reviews.prepare_review || {}
      return review.decision === "interview" ? "interview" : "reject"
    }
  }

  node schedule_interview {
    type: code
    label: "Schedule interview"
    code: @ts {
      const review = context.reviews.prepare_review || {}
      return {
        status: "interview",
        name: context.nodes.root.output.name,
        email: context.nodes.root.output.email,
        role: context.nodes.root.output.role,
        score: context.nodes.evaluate.output.score,
        reviewer_notes: review.notes || ""
      }
    }
  }

  node review_rejection {
    type: email
    label: "Send rejection after review"
    from: @ts { return "[email protected]" }
    to: @ts { return context.nodes.root.output.email }
    subject: @ts { return "Update on your application for " + context.nodes.root.output.role }
    text: @ts {
      return "Hi " + context.nodes.root.output.name + ",\n\nThank you for applying. After careful review, we have decided to move forward with other candidates. We appreciate your interest and wish you the best."
    }
  }

  node send_rejection {
    type: email
    label: "Send rejection"
    from: @ts { return "[email protected]" }
    to: @ts { return context.nodes.root.output.email }
    subject: @ts { return "Update on your application for " + context.nodes.root.output.role }
    text: @ts {
      return "Hi " + context.nodes.root.output.name + ",\n\nThank you for applying. After careful review, we have decided to move forward with other candidates. We appreciate your interest and wish you the best."
    }
  }

  node maybe_pool {
    type: code
    label: "Add to maybe pool"
    code: @ts {
      return {
        status: "maybe_pool",
        name: context.nodes.root.output.name,
        email: context.nodes.root.output.email,
        role: context.nodes.root.output.role,
        score: context.nodes.evaluate.output.score
      }
    }
  }

  flow {
    root -> evaluate
    evaluate -> route
    route -["advance"]-> prepare_review
    route -["pass"]-> send_rejection
    route -["maybe"]-> maybe_pool
    prepare_review -> review_decision
    review_decision -["interview"]-> schedule_interview
    review_decision -["reject"]-> review_rejection
  }
}

trigger on_application {
  form:job_application -> screen_application
  enabled: true
}

Flow

Trigger to workflow

Workflow nodes