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.
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 );
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 >
);
}
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 } ` );
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.
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 );
}
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
});
}
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' ,
},
});
}
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' );
});
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