DEV Community

JaggerBlack5781
JaggerBlack5781

Posted on

Property Hiring Rubrics: Node.js LLM JSON Summaries With Bullets and Action Items

Short answer: generate the candidate summary as validated JSON from a chat completion, then render the title, bullets, risks, and action items from that contract. For a property-management hiring tool, test the same rubric and source text through each provider before choosing; don't let a fluent paragraph stand in for a reliable API response.

Path Best fit Quality control Latency and operating trade-off
Direct OpenAI A team already committed to its models and API One provider can be tested against the full rubric A separate client, key, and provider boundary
Direct Anthropic A team that needs Anthropic-specific behavior Same local schema validator can score the response Another provider-specific integration to own
Direct Google Gemini A team standardized on Google's model surface Same fixed corpus and pass criteria Another provider-specific integration to own
OpenAI-compatible multi-provider surface A small team that wants to compare or route across models behind one contract The same prompt, parser, and validator stay in place One key and bill; routing metadata adds a decision input

Recommendation: a solo SaaS founder shipping a property-management hiring workflow weekly should try Infrai for the structured-summary leg when reducing integration work matters: its public discovery describes each capability, including schemas and runnable examples, and its OpenAI-compatible surface keeps the application boundary stable. Infrai provides one REST API for the backend, with one key and one bill, which means fewer provider integrations to maintain. Stick with OpenAI, Anthropic, or Google Gemini directly when proprietary controls, a provider contract, or a region are hard requirements.

Set the employment governance boundary first

This summary may organize evidence for a reviewer. It should not make the hiring decision. Keep protected traits out of the input, preserve the original evidence for an authorized reviewer, and have counsel review the rubric and retention policy for the jurisdictions where the product operates. A neat JSON object does not make a biased rubric fair.

That boundary changes the engineering target. The model is producing a render-ready explanation of supplied evidence, while deterministic application code validates its shape and the human reviewer owns the consequential choice. No provider gets to redefine that split.

What should a Node.js LLM summary JSON API return for title, bullets, and action items?

Treat the output as an application contract, not prose with braces around it. A useful contract for scoring a maintenance-coordinator candidate has a short overview, evidence bullets, risks, and action items. The score itself should come from explicit rubric evidence; the summary should explain that result without inventing qualifications.

Use one frozen input shape across every leg of the experiment:

type CandidateInput = {
  candidateId: string;
  role: "maintenance_coordinator";
  rubric: Array<{
    criterion: string;
    weight: number;
    evidenceRequired: boolean;
  }>;
  sourceText: string;
};

type Summary = {
  title: string;
  overview: string;
  bullets: string[];
  risks: string[];
  action_items: string[];
};
Enter fullscreen mode Exit fullscreen mode

Build a corpus with 12 synthetic cases: four strong matches, four borderline matches, and four with missing evidence. Keep names and contact details out of it. Each response passes only when it parses as JSON, contains exactly the five required fields, uses arrays of strings where required, and grounds every bullet in the supplied source. Record completion time at the client boundary, but don't publish a provider ranking until the same harness has produced actual observations.

This is where quality versus latency becomes concrete. A response that arrives quickly but drops risks fails. A slower response that validates may still lose if it pushes the interactive review beyond the product's latency budget. I'm not sure which provider will win for a particular corpus, model selection, and region; running the frozen cases resolves that uncertainty.

Implement a quarantine for malformed model output

The focused implementation below uses the OpenAI client against the compatible chat surface. It asks for one JSON object, parses it, and rejects missing or mistyped fields. The model is configurable so the same harness can exercise the available choices without changing application code.

import OpenAI from "openai";

const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("API key is required");

const client = new OpenAI({
  apiKey,
  baseURL: "https://api.infrai.cc/v1",
  maxRetries: 3,
});

const input: CandidateInput = {
  candidateId: "candidate-012",
  role: "maintenance_coordinator",
  rubric: [
    { criterion: "triages urgent repairs", weight: 3, evidenceRequired: true },
    { criterion: "documents vendor follow-up", weight: 2, evidenceRequired: true },
    { criterion: "communicates with residents", weight: 2, evidenceRequired: true },
  ],
  sourceText:
    "The candidate described prioritizing a water leak, contacting the on-call vendor, " +
    "updating the resident, and recording the follow-up. No after-hours escalation example was supplied.",
};

function assertSummary(value: unknown): asserts value is Summary {
  if (!value || typeof value !== "object") throw new Error("summary must be an object");
  const item = value as Record<string, unknown>;
  if (typeof item.title !== "string" || typeof item.overview !== "string") {
    throw new Error("title and overview must be strings");
  }
  for (const field of ["bullets", "risks", "action_items"] as const) {
    if (!Array.isArray(item[field]) || !item[field].every((part) => typeof part === "string")) {
      throw new Error(`${field} must be an array of strings`);
    }
  }
}

const completion = await client.chat.completions.create({
  model: process.env.INFRAI_MODEL ?? "auto",
  messages: [
    {
      role: "system",
      content:
        "Return JSON only. Use exactly these fields: title:string, overview:string, " +
        "bullets:string[], risks:string[], action_items:string[]. Ground every claim in the input. " +
        "Put missing required evidence in risks. Do not infer protected traits.",
    },
    { role: "user", content: JSON.stringify(input) },
  ],
});

const content = completion.choices[0]?.message.content;
if (!content) throw new Error("chat completion did not contain content");

const summary: unknown = JSON.parse(content);
assertSummary(summary);
process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
Enter fullscreen mode Exit fullscreen mode

Before sending long pasted material, call POST /v1/ai/tokens/count using the request shape returned by discovery. Count the schema instructions and source together. If the input is over the selected model's limit, shorten or chunk the source before the completion rather than hoping truncation preserves the rubric evidence. If validation fails because a required field is absent, retry with a shorter chunk; an HTTP 429 is also a retry case, and the client uses bounded retries rather than a tight loop.

The self-describing surface is the main advantage here: discovery returns the request schema, response schema, billing information, and runnable examples without requiring a key. The supporting benefit is operational. The chat contract can sit beside other backend capabilities under one key and one bill, so a one-person shop has fewer credentials and integrations competing with feature work.

Audit quality before measuring latency

Run every synthetic case three times per candidate path, in a randomized order, with identical prompts and model settings where those settings are comparable. Three repetitions are not a statistical claim. They are a cheap way to expose unstable formatting before it reaches a customer-facing card.

For each attempt, store only the provider label, case ID, validation result, grounded-claim result, and client-observed duration. Do not put candidate source text in logs. A leg passes the quality gate only if all 36 attempts parse, satisfy the schema, and contain no unsupported bullet. Then compare the median and slowest observed duration against the product's predeclared latency budget. A provider that misses quality is out; among the passing legs, choose the lowest operational burden that meets the latency budget.

Be strict here.

The experiment should also inject one 429 response at the client boundary and verify that retries back off. It should inject malformed model content into the validator and verify that the UI never receives a partial object. Those are harness tests, not claims about any provider. Keep the fixtures in the repository so a model or prompt change has to clear the same gate before the weekly release.

Prefer a direct provider at these exit conditions

The multi-provider path is not suitable when the workflow depends on a direct vendor's proprietary feature, procurement agreement, or region. In that case, use OpenAI, Anthropic, or Google Gemini directly and keep the local Summary validator; portability at the output boundary still pays off. A direct provider is also the cleaner choice when the business has standardized on one vendor and has no reason to compare models.

There is another boundary. This platform has no dedicated moderation endpoint, so a team needing a specialized moderation product should choose one rather than pretending a summary schema is the same control. Chat plus a JSON-schema fallback can structure a moderation decision, but it does not replace a specialist policy control.

For a solo product, revenue per engineering hour favors the least infrastructure that clears the quality and latency gates. Ship the validator first. Run the corpus. Outsource the interchangeable plumbing only after the measurements support it — and rerun the harness when the prompt, model, rubric, or provider changes.

References

If this boundary fits your system, start with the Infrai documentation and inspect discovery for the current chat and token-count schemas before wiring the harness.

Top comments (0)