A trigger procedure must be an external ABL procedure. That is, it must be a procedure file of its own, not an internal procedure within some larger procedure file. It can contain just about any ABL code. It is identified by a special header statement at the top of the procedure file.

CREATE, DELETE, and FIND headers

The header statement for a CREATE, DELETE, or FIND procedure has this syntax:
TRIGGER PROCEDURE FOR { FIND | CREATE | DELETE } OF table-name.

This statement effectively defines a buffer automatically with the same name as the table, scoped to the trigger procedure.

WRITE header

The header statement for a WRITE trigger has this syntax:
TRIGGER PROCEDURE FOR WRITE OF table-name
  [ NEW [ BUFFER ] new-buffer-name ]
  [ OLD [ BUFFER ] old-buffer-name ].

When executing a WRITE trigger, the AVM makes two record buffers available to the trigger procedure. The NEW buffer contains the modified record that is being validated. The OLD buffer contains the most recent version of the record before the latest set of changes was made. This is the template record that holds initial values for a table if it is a newly-created record, the record from the database if the record has not been validated, or the most recently validated record if it has been validated. The default for the new buffer is the automatically-created buffer named for the table itself, which makes the NEW phrase optional. If you wish to compare the new buffer to the old, you must use the OLD phrase to give the old one a name. The BUFFER keyword is just optional syntactic filler.

You can make changes to the NEW buffer, but the OLD buffer is read-only.

You can determine whether the record being written is newly created using the ABL NEW function, which returns true if the AVM has not written the record to the database before, and false otherwise:
NEW table-name
For example:
IF NEW Customer THEN ...

ASSIGN header

The header statement for an ASSIGN trigger has this syntax:
TRIGGER PROCEDURE FOR ASSIGN { OF table.field }
    | NEW [VALUE] new-field { AS data-type | LIKE other-field }
    [ OLD [VALUE] old-field { AS data-type | LIKE other-field2 } ] .

If you use the OF form, the expression table.field identifies the field, but you can in fact refer to any field in the record where the field has been changed.

If you need to compare the field value before and after it was changed, you must use the NEW and OLD phrases to give those versions of the field names. If you do this, you cannot refer to the rest of the record buffer. You can change the NEW field, and this changes the field value in the record, but changing the OLD field value has no effect. The VALUE keyword here is just optional syntactic filler.