Healthcare integration work keeps landing on data teams that were never staffed for it. Payer and provider interoperability rules have pushed FHIR APIs from pilot to production, while the HL7 v2 feeds behind them are not going anywhere. Most shops end up running both at once: a pipe-delimited ADT feed from a 1998 interface engine, and a FHIR R4 endpoint that wants OAuth 2.0 and paginated Bundles.
Talend handles both, but not with the components most people reach for first. Here is the pattern we use.
1. HL7 v2: treat it as hierarchical text, not flat file
An HL7 v2 message is segments separated by carriage returns (\r, not \n — this breaks more jobs than anything else), fields by |, components by ^, and repeats by ~. A single ADT^A01 can carry multiple PID, NK1, OBX, and IN1 segments.
Do not try to model it as one wide tFileInputDelimited schema. You will spend a month on edge cases. Two options that work:
Option A — HAPI in a routine. Add the HAPI HL7v2 library to the job's Maven dependencies (or drop the jars in via tLibraryLoad on older Studio versions) and write a Java routine that takes the raw message string and returns a parsed object or a JSON string. tJavaFlex or tJavaRow then emits one row per segment type. This gives you real validation, version-aware structures, and useful error messages when a sender drifts.
Option B — split and iterate. Read the file with tFileInputFullRow (delimiter \r), route segments with tMap filters on substring(0,3), and parse each segment type with its own tExtractDelimitedFields using |. Cheaper to stand up, fine for a handful of message types, painful once OBX-5 starts carrying varying value types.
Either way, land a normalized staging model rather than a single denormalized table:
msg_header— control ID, sending facility, message type, timestamp, raw payload hashmsg_patient— MRN, assigning authority, names, DOB, gendermsg_observation— one row perOBX, typed byOBX-2msg_raw— the full original message, always
Keeping the raw message is not optional. When a clinician disputes a value six months later, the only defensible answer is the bytes you received.
Idempotency
Interface engines resend. Use MSH-10 (message control ID) plus sending facility as the natural key and MERGE on it, or dedupe in staging with tUniqRow before the load. Never assume a message arrives once.
2. FHIR R4: REST plus pagination plus OAuth
FHIR is ordinary JSON over HTTPS, so tRESTClient or tHTTPClient works — with three wrinkles.
Auth. Most production servers use SMART on FHIR backend services: sign a JWT with your private key, POST it to the token endpoint as a client_credentials grant, get a short-lived bearer token. Build this as a subjob that runs once, stores the token and its expiry in context variables, and refreshes when expires_in is close. Our earlier walkthrough on calling OAuth 2.0 APIs from Talend covers the token-refresh mechanics in detail.
Pagination. A search response is a Bundle whose link array contains a relation: "next" entry with a full URL. The loop is: call, extract entries, look for next, set the context URL, repeat. In Talend that is a tLoop with a condition on a context boolean, or a tFlowToIterate over the next-link. Do not paginate with _offset arithmetic; servers are allowed to invalidate it.
Extraction. tExtractJSONFields with JSONPath $.entry[*].resource gives you one row per resource. For nested repeating data — name[0].given[*], telecom[*], address[*] — use a second tExtractJSONFields fed by the resource JSON string rather than trying to flatten in one pass. Store the full resource JSON in a jsonb/VARIANT column alongside your typed columns; FHIR extensions will otherwise be silently discarded.
3. Bulk Data: $export and NDJSON
For population-scale pulls, resource-by-resource paging is the wrong tool. FHIR Bulk Data ($export) is an async pattern:
GET /Group/{id}/$export?_type=Patient,Observationwith headerPrefer: respond-async.- The server returns
202 Acceptedand aContent-Locationpolling URL. - Poll that URL until it returns
200with a manifest of file URLs. RespectRetry-After; some exports take hours. - Download each NDJSON file — one JSON object per line, often gzipped and often multi-gigabyte.
Model step 2–3 as a tLoop with tSleep and a tJavaRow that checks the status code, not as a fixed wait. Then read the NDJSON with tFileInputFullRow (one line = one row) into tExtractJSONFields, so memory stays flat regardless of file size. Never load an export file with tFileInputJSON in document mode — it will parse the whole thing into memory.
Write each file's contents to staging with the export job ID and file URL attached, so a failed download can be retried at file granularity instead of restarting a three-hour export.
4. PHI handling that survives an audit
The engineering decisions here are small; the consequences are not.
- Keep PHI out of logs.
tLogRowon a patient flow writes names and MRNs into job logs and into the Talend Management Console run output. Use a dedicated error flow that logs keys and row counts only, per our error handling and observability patterns. - Encrypt at rest and in transit, including the landing zone for NDJSON files. Delete downloaded export files on a schedule; a bulk export sitting on a JobServer disk is a breach waiting to happen.
- Mask for non-production.
tDataMaskingwith consistent, referentially intact substitution keeps test data usable — see masking PII in Talend. - Secrets out of contexts. Private keys for SMART backend services belong in a vault, not
context.properties. Rotate them on a schedule and make the job fetch at runtime. - Record provenance. Source system, message control ID or FHIR
versionId, retrieval timestamp, and job run ID on every row. Auditors ask where a value came from, and "the warehouse" is not an answer.
5. What usually goes wrong
- Line endings. HL7 uses
\r. Windows-edited test files use\r\n. Your parser must tolerate both. - Z-segments. Every site invents custom
Zxxsegments. Route unknown segments to a quarantine table rather than failing the message. - Timezones. HL7 timestamps may carry an offset or none at all. Decide the default per sending facility and store it in config, not in code.
- Empty vs. null. In FHIR, an absent element and an explicitly null one mean different things for updates. Keep the raw JSON so you can tell them apart later.
- Terminology. LOINC, SNOMED, ICD-10, and local codes will all appear in the same column. Map in a lookup table you can version, not in a
tMapexpression.
Where this lands
The durable architecture is boring on purpose: raw landing zone, normalized staging, typed conformed tables, and a warehouse load that is fully rerunnable. Talend is a good fit for the messy front half — protocol handling, parsing, validation, retry — and increasingly hands the modeling half to ELT in the warehouse.
If you are standing up HL7 or FHIR ingestion, migrating an aging interface feed onto Qlik Talend Cloud, or trying to make an existing clinical pipeline auditable, get in touch. We have built these pipelines under real compliance pressure and can tell you quickly which half of your problem is actually hard.