Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

SWIFT MT Format

Clinker reads and writes SWIFT MT (FIN) messages alongside CSV, JSON, XML, fixed-width, EDIFACT, X12, and HL7 v2. A SWIFT MT message is a finite file built from brace-balanced blocks. The reader streams the message body one field at a time and surfaces the service blocks as document-envelope sections; the writer inverts the reader exactly, re-framing the block structure around emitted records so a read → write → read round-trip is byte-faithful.

Block structure

A SWIFT MT message is a sequence of top-level blocks, each {n:...} where n is a numeric block id:

BlockRoleContents
1Basic headerA fixed header string (application id, BIC, …)
2Application headerInput/output direction, message type, recipient
3User headerOptional; may carry nested {tag:value} sub-blocks
4Message textThe message body — a run of :tag:value fields
5TrailerOptional; may carry nested {tag:value} sub-blocks

Unlike the flat delimiter-structured EDI formats (HL7, X12, EDIFACT), SWIFT framing is brace-balanced rather than terminator-delimited. Because of that, the nested sub-blocks of blocks 3 and 5 (for example {3:{108:MSGREF}}) are kept intact inside their parent block rather than mistaken for top-level blocks.

The -} text-block trailer

Block 4 is special. Its body is opaque line-structured free text — a field value (a :77E: envelope, a :79: narrative, an :86: information line) legitimately contains {, }, and even -} as data. So braces inside block 4 are treated as data, not framing: the block closes only on a line-anchored -} trailer — a -} that begins a line. An interior {, }, or a -} in the middle of a value is data, not a frame boundary. Both the framing braces and the closing -} trailer are stripped from the stored values, so a record carries clean tag/value data.

Whitespace between blocks

Producers insert CR/LF (and the \r\n that separates block-4 fields) for readability. Inter-block whitespace is insignificant and is skipped; the line breaks inside block 4 delimit the :tag:value fields.

Record shape

Each :tag:value line of block 4 becomes one record under a fixed positional schema — the same one-line-one-record model the X12 and HL7 readers use:

ColumnMeaning
blockThe block id the line came from (always 4 for body fields)
tagThe SWIFT field tag without its surrounding colons (20, 32A, 61)
valueThe field value, with continuation lines folded in verbatim

A multi-line field (a :50K: ordering-customer block, a :77E: / :86: narrative) keeps its continuation lines: any line of block 4 that does not begin a new :tag: is folded into the current field’s value with its line break preserved — including a blank line inside the value, so a narrative with an internal blank line round-trips faithfully. A repeated tag (the :61: / :86: statement lines of an MT940, for instance) streams as one record per occurrence, in order.

The service blocks (1, 2, 3, 5) are consumed by the reader to serve envelope sections and drive the message-level document context — they are never emitted as body records.

nodes:
  - type: source
    name: payments
    config:
      name: payments
      type: swift
      glob: ./inbox/*.swift
      options:
        max_fields: 20000   # raise the block-4 field ceiling for large messages
      schema:
        - { name: block, type: string }
        - { name: tag, type: string }
        - { name: value, type: string }

The max_fields option caps the number of block-4 field lines a single message may carry (default 10000). A message exceeding it is rejected with guidance rather than streamed unbounded — a corruption guard, since a real MT message is well under the cap.

Envelope sections over the service blocks

A SWIFT MT message is a single envelope: the four service blocks surface as file-level $doc sections, exposing each block’s text to CXL as $doc.<section>.body. Every body record can read the enclosing message’s headers through a $doc.<section>.body lookup.

Declare the sections on the source with the segment extract rule naming the block id. The whole block body surfaces under the field name body, because a SWIFT service block carries free-form text (a header string, nested {sub:tag} blocks) rather than positional elements:

envelope:
  sections:
    basic:
      extract: { segment: "1" }   # block 1, the basic header
    app:
      extract: { segment: "2" }   # block 2, the application header
    user:
      extract: { segment: "3" }   # block 3, the user header (nested sub-blocks kept verbatim)
    trailer:
      extract: { segment: "5" }   # block 5, the trailer

A Transform on any body record can read every enclosing block at once:

emit tag       = tag
emit value     = value
emit basic_hdr = $doc.basic.body     # block 1 header string
emit app_hdr   = $doc.app.body       # block 2 header string
emit user_hdr  = $doc.user.body      # block 3 body, nested {108:...} kept verbatim

The section names are entirely your choice — the engine reserves none. A segment extract may name a block either by its numeric id ("1", "3") or by the stable default label ("basic_header", "app_header", "user_header", "trailer"); both resolve the same block.

Block 4 is the message-text body streamed as records, not an envelope section — a segment: "4" extract is rejected at startup. An xml_path or json_pointer extract against a SWIFT source is likewise rejected, because those rules belong to the tree formats.

Malformed-message handling

A structurally broken message fails the run with a precise SWIFT error rather than producing garbled records:

  • Unbalanced brace — a block that never closes (or whose brace depth never returns to zero) is a truncation error naming the offending block.
  • Missing -} trailer — a block 4 that runs to end of input without its -} trailer is a truncation error.
  • Missing or non-numeric block id — a block without a numeric id after the { (or with no : separating the id from the body) is rejected.
  • Malformed :tag:value line — a block-4 line with no second colon closing the tag, or an empty tag, is rejected.
  • Repeated service block — a second {1:...} (or any repeated service block) in one message is rejected.

A header-only message (no block 4, or an empty block 4) is valid: it produces no body records and drains cleanly.

Writing SWIFT MT

A SWIFT Output node inverts the reader exactly. It re-emits each block-4 record as a :tag:value line and re-frames the single message envelope around them: the service blocks 1/2/3 first, then block 4 ({4:-}), then the optional block-5 trailer. Block-4 free text is opaque, so values are written verbatim with no escaping — an interior {, }, a mid-line -}, a folded continuation break, and an interior blank line all reproduce as data. The reader strips exactly the structural separators (braces, the leading :, the -} trailer, the line breaks) and keeps every other byte; the writer re-adds exactly those separators and nothing else, so a read → write → read round-trip returns byte-identical field values.

Records map by the tag and value columns. The block column is the constant 4 discriminator (an empty block is treated as block 4, so a Transform that projects only tag/value writes fine); a record carrying a block other than 4 is rejected, because service blocks are never emitted as records — they ride the document context.

nodes:
  - type: output
    name: out
    input: messages
    config:
      name: out
      type: swift
      path: ./out/message.swift
      options:
        basic_header_from_doc: basic
        app_header_from_doc: app
        user_header_from_doc: user
        trailer_from_doc: trailer

Each service block is written from a literal body or echoed from a user-declared $doc section:

OptionMeaning
basic_headerLiteral block-1 body, written verbatim as {1:<body>}.
basic_header_from_docName of a $doc section to echo the block-1 body from.
app_headerLiteral block-2 body.
app_header_from_docName of a $doc section to echo the block-2 body from.
user_headerLiteral block-3 body (nested {sub:tag} content kept verbatim).
user_header_from_docName of a $doc section to echo the block-3 body from.
trailerLiteral block-5 body, written after block 4 closes.
trailer_from_docName of a $doc section to echo the block-5 body from.

The *_from_doc options name the section the user declared on the source — the engine reserves no section name. A literal *_header wins over its *_from_doc companion when both are set; a service block with neither is omitted. The _from_doc echo reads the block body verbatim from the section’s body field — the same single-field shape the reader writes — so a SWIFT source’s service blocks (declared as segment envelope sections) round-trip unchanged when their section names are passed back to the writer here.

The document context that carries the $doc sections rides on each body record, so the *_from_doc echoes require at least one block-4 record to read from. A document with zero block-4 records emits a valid empty message — {4: immediately closed by -} — wrapped only by the literal-configured service blocks (basic_header, app_header, user_header, trailer); the *_from_doc echoes are skipped because no record carries the document context. Use the literal options when a zero-record output must still emit its service blocks.

Block-4 free text has no escape mechanism, so values are written verbatim. Almost any value round-trips faithfully, but two shapes are unrepresentable when the value is built from arbitrary records (CSV/JSON → Transform → SWIFT): a value whose continuation line — a line after a folded line break — begins with the block terminator -} (which would re-read as an early block close) or with a : tag marker (which would re-read as a spurious field). The writer rejects such a value with a clear error rather than emitting silently-corrupt output. Values read from a SWIFT source can never take these shapes, so a read → write → read round-trip is always safe.

A SWIFT MT message is a single indivisible envelope, so a swift output cannot be combined with a byte-limit split: block — the pairing is rejected at config-validation time (diagnostic E342).

Limitations

  • UTF-8 only. SWIFT MT messages are decoded as UTF-8; a non-UTF-8 block body is rejected explicitly rather than corrupted silently.
  • Field-content parsing. The reader exposes each :tag:value line as a tag/value pair verbatim. Parsing a field’s internal structure (the sub-fields of a :32A: value-date/currency/amount, say) is a CXL concern downstream of the source, not a reader responsibility.