Skip to main content
This guide walks through the complete integration flow from job setup to candidate scoring.

Architecture Overview

Phase 1: Job Setup

This happens once per job posting, typically when the job is finalized.
1

Generate Clarification Questions (Optional)

Call /v1/criteria/questions to get questions that help calibrate the criteria.
const questionsResponse = await nova.post('/v1/criteria/questions', {
  jobContext: {
    jobId: job.id,
    jobTitle: job.title,
    companyName: company.name,
    jobDescription: job.description,
    additionalInfo: job.internalNotes,
  },
});

// Store questionSetId for later
await db.jobPosting.update({
  where: { id: job.id },
  data: { novaQuestionSetId: questionsResponse.questionSetId },
});

// Present questions to your team
await presentQuestions(questionsResponse.questions);
2

Collect Answers

Present the questions in your UI and collect answers.
// Your UI component
function CriteriaQuestionsForm({ questions, onSubmit }) {
  const [answers, setAnswers] = useState({});

  return (
    <form onSubmit={() => onSubmit(answers)}>
      {questions.map(q => (
        <QuestionInput
          key={q.id}
          question={q}
          value={answers[q.id]}
          onChange={(value) => setAnswers({...answers, [q.id]: value})}
        />
      ))}
      <button type="submit">Generate Criteria</button>
    </form>
  );
}
3

Generate Criteria

Call /v1/criteria/generate with the job context and answers. Criteria are stored server-side automatically.
const criteriaResponse = await nova.post('/v1/criteria/generate', {
  jobContext: {
    jobId: job.id,
    jobTitle: job.title,
    companyName: company.name,
    jobDescription: job.description,
  },
  questionSetId: job.novaQuestionSetId,
  answers: formattedAnswers,
});

// Mark job as ready for Nova scoring
// No need to store criteria - they're managed by Nova
await db.jobPosting.update({
  where: { id: job.id },
  data: { novaEnabled: true },
});

console.log(`Generated ${criteriaResponse.criteria.length} criteria for job ${job.id}`);
4

Display Criteria for Review

Show the generated criteria for review.
function CriteriaReview({ criteria, onApprove, onRegenerate }) {
  return (
    <div>
      <h3>Generated Screening Criteria</h3>
      {criteria.map(c => (
        <CriterionCard
          key={c.id}
          text={c.text}
          importance={c.importance}
        />
      ))}
      <button onClick={onApprove}>Approve & Activate</button>
      <button onClick={onRegenerate}>Regenerate</button>
    </div>
  );
}

Phase 2: Application Scoring

This happens for each candidate application.
1

Receive Application

When a candidate applies, prepare their data for scoring.
async function handleNewApplication(application) {
  const job = await db.jobPosting.findUnique({
    where: { id: application.jobId },
  });

  // Only score if Nova is enabled for this job
  if (!job.novaEnabled) {
    return; // Job not set up for Nova scoring
  }

  await submitForScoring(application, job);
}
2

Generate Resume URL

Create a pre-signed URL for the resume. URLs must be valid for at least 2 hours; we recommend 24 hours to handle retries and edge cases.
async function getResumeUrl(application) {
  // Example for AWS S3
  const command = new GetObjectCommand({
    Bucket: 'resumes',
    Key: application.resumeKey,
  });

  return getSignedUrl(s3Client, command, {
    expiresIn: 86400 // 24 hours
  });
}
3

Submit for Scoring

Call /v1/score with the candidate data. No criteria needed - Nova loads them automatically.
async function submitForScoring(application, job) {
  const resumeUrl = await getResumeUrl(application);

  const response = await nova.post('/v1/score', {
    jobId: job.id,
    applicationId: application.id,
    candidate: {
      resumeUrl,
      applicationAnswers: application.answers,
    },
  });

  // Store scoring job ID for tracking
  await db.application.update({
    where: { id: application.id },
    data: {
      novaScoringJobId: response.scoringJobId,
      novaStatus: 'scoring',
    },
  });
}
4

Handle Webhook

Process the scoring result when it arrives. See Webhook Signature Verification for the full verifySignature implementation using HMAC-SHA256.
app.post('/webhooks/nova', async (req, res) => {
  // Verify signature - see webhooks docs for implementation
  if (!verifySignature(req)) {
    return res.status(401).send('Invalid signature');
  }

  const event = req.body;

  if (event.event === 'score.completed') {
    await db.application.update({
      where: { id: event.applicationId },
      data: {
        novaScore: event.result.score,
        novaAssessment: event.result.assessment,
        novaStatus: 'completed',
      },
    });
  } else if (event.event === 'score.failed') {
    await db.application.update({
      where: { id: event.applicationId },
      data: {
        novaError: event.error,
        novaStatus: 'failed',
      },
    });
  }

  res.status(200).send('OK');
});
5

Display Results

Show the score and assessment in your applicant view.
function ApplicantScore({ application }) {
  if (application.novaStatus === 'scoring') {
    return <ScoringInProgress />;
  }

  if (application.novaStatus === 'failed') {
    return <ScoringError error={application.novaError} />;
  }

  return (
    <div>
      <ScoreBadge score={application.novaScore} />
      <AssessmentVerdict text={application.novaAssessment.verdict} />
      <StrengthsList items={application.novaAssessment.strengths} />
      <ConcernsList items={application.novaAssessment.concerns} />
      <InterviewFocus items={application.novaAssessment.interviewFocus} />
    </div>
  );
}

Error Handling

If scoring fails with RESUME_FETCH_FAILED, generate a new URL and resubmit.
if (event.error.code === 'RESUME_FETCH_FAILED') {
  const newUrl = await getResumeUrl(application);
  await submitForScoring(application, job); // Retry
}
If scoring fails with CRITERIA_NOT_FOUND, the job doesn’t have criteria generated yet.
if (event.error.code === 'CRITERIA_NOT_FOUND') {
  // Job wasn't set up properly - generate criteria first
  await generateCriteriaForJob(job);
  await submitForScoring(application, job); // Retry
}
Poll for results as a fallback:
// Run periodically for applications stuck in 'scoring'
async function checkPendingScores() {
  const stuckApplications = await db.applications.findMany({
    where: {
      novaStatus: 'scoring',
      submittedAt: { lt: new Date(Date.now() - 5 * 60 * 1000) } // 5+ min ago
    }
  });

  for (const app of stuckApplications) {
    const result = await nova.get(`/v1/score/${app.scoringJobId}`);
    if (result.status === 'completed' || result.status === 'failed') {
      await handleScoreResult(result);
    }
  }
}
Queue requests and respect Retry-After:
class ScoringQueue {
  async add(application) {
    this.queue.push(application);
    await this.processQueue();
  }

  async processQueue() {
    while (this.queue.length > 0 && this.concurrency < 10) {
      const app = this.queue.shift();
      this.concurrency++;

      try {
        await this.score(app);
      } catch (error) {
        if (error.status === 429) {
          this.queue.unshift(app); // Re-queue at front
          await sleep(error.retryAfter * 1000);
        }
      } finally {
        this.concurrency--;
      }
    }
  }
}

Database Schema Suggestions

-- Add Nova fields to your job postings table
ALTER TABLE job_postings ADD COLUMN nova_question_set_id VARCHAR(255);
ALTER TABLE job_postings ADD COLUMN nova_enabled BOOLEAN DEFAULT false;

-- Add Nova fields to your applications table
ALTER TABLE applications ADD COLUMN nova_scoring_job_id VARCHAR(255);
ALTER TABLE applications ADD COLUMN nova_status VARCHAR(50); -- 'pending', 'scoring', 'completed', 'failed'
ALTER TABLE applications ADD COLUMN nova_score INTEGER;
ALTER TABLE applications ADD COLUMN nova_assessment JSONB;
ALTER TABLE applications ADD COLUMN nova_error JSONB;
Notice there’s no nova_criteria column needed. Criteria are stored and managed by Nova - you just track whether Nova is enabled for the job.

Checklist

Job setup flow implemented (questions → answers → criteria)
Job marked as Nova-enabled after criteria generation
Resume URL generation with proper expiry
Score submission on new applications
Webhook endpoint with signature verification
Score display in applicant view
Error handling and retry logic
Fallback polling for missed webhooks