A transform map gives you eight places to hang a script, and picking the wrong one is the most common reason an import behaves oddly. The difference between them is not what they can do - it is when they run and, as a direct consequence, what exists at that moment. Get that wrong and you are reading a field that has not been populated yet, or writing to a record the platform is about to overwrite.
This is the order things happen in, and what is available at each point.
onStart - once, before anything is read
Runs a single time, before the first row is touched. Neither the source nor the target row exists yet, so there is nothing to read and nothing to set. Use it for setup that applies to the whole run: initialising a counter, stashing something on the import set, logging that the job began.
Reaching for source or target here is the classic mistake. They are not populated.
onBefore - once per row, before the write
Runs for every row, after the source row has been read but before the target record is inserted or updated. Both source and target are available, and changes to target land in the saved record.
This is where most real work belongs: normalising a value, resolving a reference by hand, deciding to skip the row entirely with ignore = true.
onAfter - once per row, after the write
Runs after the target record has been saved, so target now has a sys_id. Anything that needs the saved record to exist goes here - creating a related record, updating a parent, kicking off a follow-up.
Setting a field on target in onAfter does not persist on its own. The write has already happened; you have to save it yourself.
onComplete - once, at the end
The counterpart to onStart. Every row has been processed, so this is where a summary belongs: how many rows succeeded, how many were ignored, a notification to whoever cares.
Field-level source script
Scoped to a single field map rather than the whole transform. It computes the value for one target field, and what it returns is what gets written. Use it when one field needs logic and the rest are straightforward - it keeps that logic next to the mapping it belongs to instead of in a row-level script that has to work out which field it is dealing with.
Record-level script
The broader version: it runs per row and can touch any field. Reach for it when several fields depend on each other, or when the decision about one field needs to see the others.
onForeignInsert - when a referenced record is about to be created
Fires when a field map with the Create choice action is about to insert a record in the referenced table. It is your chance to shape or validate that record before it exists - set the fields the reference table requires, or reject it.
Without this, imports quietly create half-populated reference records. That is the origin of most "why do we have 400 empty locations" conversations.
onChoice - deciding what to do with an unmatched value
Runs when the incoming value does not match an existing choice or referenced record, and decides which action the platform takes: create it, ignore the row, or reject it. Returning the decision from a script rather than fixing it on the field map is what lets the same transform handle both clean and messy source data.
onReject - when a row is turned away
Runs when a row has been rejected, whichever script rejected it. Nothing is going to be written at this point, so the value of onReject is entirely in what you record: log the row, count it, notify someone. An import that silently drops rows is worse than one that fails.
The variables, and where each one is real
Every transform script runs with the same set of variables in scope. The catch is that being in scope is not the same as being populated - source and target exist as names in onStart and contain nothing.
source- a GlideRecord of the import set row being read. Available from onBefore onwards.target- a GlideRecord of the record being written. Available from onBefore; only has asys_idfrom onAfter.map- the transform map record being executed.import_set- the import set the run belongs to.action- whether this row is aninsertor anupdate.name- in a field-level script, the target field currently being processed.value- in a field-level script, the incoming source value for that field.ignore- set totrueto skip the row without treating it as an error.error- set totrueto stop the transformation and report a failure.error_message- the text reported whenerroris set.status_message- a message returned in the response, for the non-error case.log- the logger for the run:log.info(),log.warn(),log.error().
Choosing, in one paragraph
If it applies to the whole run, onStart or onComplete. If it applies to a row and needs to affect what is saved, onBefore. If it needs the saved record, onAfter. If it is about one field, a field-level source script. If it is about a referenced record being created, onForeignInsert. If it is about what to do with data that does not match, onChoice, and record what happened in onReject.
Nearly every transform bug we are asked to look at is one of these in the wrong slot - usually work that belongs in onBefore sitting in onAfter, where it runs but does not persist.
If you have an import that drops rows, duplicates them, or writes values nobody can account for, tell us what you are importing.
