FormTemplate at the end. The client polls the job for progress.
Source: backend/apps/core-api/src/modules/forms-ai.
Auth
Web lane.req.auth must carry studioId and userId, otherwise the controller throws UNAUTHORIZED (401). The controller’s callerOf also refuses the TRAINEE role with FORBIDDEN (403, coaches only). OWNER, HEAD_COACH and SUB_COACH are allowed.
How a job runs
analyze() writes the initial state to Redis and schedules runJob with setImmediate. There is no BullMQ queue. The job runs inside the API process that received the request, so a process restart loses a running job and the client only sees the state stop changing until the key expires.
State lives in one Redis key, perform:forms-ai:<jobId>, with a 3600 second expiry that is refreshed on every progress write. The stored state includes the owner’s studioId and userId.
Source priority
- Link. The first
http(s)URL found intextwins, and files are ignored. The page is fetched throughai/safe-fetch.tswith the user agentPerformFormsBot/1.0.- A page that contains
FB_PUBLIC_LOAD_DATA_is a Google Form.parseGoogleFormHtmlreads the embedded data deterministically with no model call. - Any other page is stripped to text (maximum 20,000 characters) and structured by the model. If the model is unavailable or returns nothing, the deterministic line parser
parseFormTextis the last resort.
- A page that contains
- Files. With no URL and at least one file, each file is transcribed by the plan-import OCR (
plan-import/ocr.ts). Transcripts are appended to any pasted text. - Text. Otherwise the pasted text is parsed by
parseFormText.
ParsedForm shape.
Model configuration
The OCR and the structurer are built fromANTHROPIC_API_KEY, AGENT_MODEL and the optional ANTHROPIC_WORKSPACE_ID. According to the wiring in forms-ai.routes.ts, both are null when no key is set. In that case pasted text and Google Forms links still work, file uploads fail with a Hebrew error, and other links fall back to the line parser.
The structurer prompt (llm-structurer.ts) tells the model to extract only what is written in the source and never invent fields, options or help text. Any model failure returns null and the caller picks the fallback.
Classification
classifyParsedForm (classifier.ts) maps each parsed field to a production field type with keyword rules and assigns a confidence:
Only fields below
LOW_CONFIDENCE_THRESHOLD (0.75) are stored, keyed by field id, in logic.aiImport.fieldConfidence. The builder uses them to mark fields the coach should review.
Generated fields get ids and keys ai_1, ai_2 and so on. Pages get ids p1, p2. The schema is created with settings.rtl: true.
Template creation
The job callscreateTemplate on a forms service built with the repository only. No submit hooks, notifier or summarizer are wired, because the job never submits or sends anything. The template is created with:
type: the requestedformTypecadence: "WEEKLY"when the type isCHECK_INname: the title found in the source, or the Hebrew type label followed byחדשwhen there was nonestatus: "DRAFT"andactive: false, which keeps it out of send pickers until the coach publishes itlogic.aiImport:source(link:<host>,filesortext),fieldConfidenceandcreatedAt
Stages
Endpoints
POST /v1/web/forms-ai/analyze
Starts an analysis job. Responds with 202.
Auth: web lane, any role except TRAINEE.
This route has its own 48mb JSON body limit in app.ts, because it can carry up to 10 base64 files.
string
required
INTAKE, CHECK_IN or ONE_TIME. PDF_SIGNATURE is not accepted.string
Pasted form content or a message containing a link. Maximum 20,000 characters.
object[]
Maximum 10 files.
text (non blank after trimming) or files (non empty) is required.
Response:
Failures inside the job are not HTTP errors. They surface in the job’s
error field.
GET /v1/web/forms-ai/analyze/:jobId
Returns the current state of a job.
Auth: web lane, any role except TRAINEE. The caller must be the same studioId and userId that started the job.
string
required
The id returned by the analyze call.
string
The job id.
string
fetch, parse, classify or create.number
Progress from 3 to 100.
boolean
true when the job finished, with either result or error.string
Present only on failure. A Hebrew message meant to be shown to the coach.
object
Present only on success:
formId, name, fieldCount, pageCount.result.formId is the new FormTemplate.id. Open it with the template endpoints on the Forms page. fieldCount comes from the forms service and excludes display-only fields.
Job failure messages (ERRORS in forms-ai.service.ts):
Errors: