Skip to main content

Byte manipulation in workflows

· 3 min read
Jeremy Scott
Co-founder

Prior to version 3.1.0 of opscotch, payload manipulation was limited to text - you could manipulate that at the byte level in JavaScript but it was very cumbersome and inefficient.

Version 3.1.0 of opscotch adds a ByteContext plus a ByteReader for low-level byte manipulation inside workflows. This is for engineers who already think in buffers, encodings, and compression.

The ByteContext gives you primitives to create, copy, combine, convert, compress, and read/write bytes. Almost every function returns or accepts a buffer handle (not raw arrays).

Importantly, the agent owns the actual memory; you work with buffer handles and maniplation instructions (via methods) and let opscotch do the heavy lifting.

Key docs: ByteContext and ByteReader.

Here is a summary of the methods:

  • Create: create(size), createFromByteArray([...]), createFromString("text"), copy(buffer)
  • Combine/move: concat([buffers...]), writeBytes(dest, offset, source, sourceOffset, length)
  • Read/write: writeByte(...), readByte(...), getSize(buffer)
  • Convert: hexToBinary, binaryToHex, base64ToBinary, binaryToBase64
  • Compress: gzip, gunzip, zip, unzip
  • Stream: reader(buffer) to read sequentially; reader() to read from the context stream
  • Release: buffers are freed automatically when the step finishes (or call release(...) explicitly)

ByteReader is a forward-only reader with read(length), readAll(), and available(). context.getStream() also returns a ByteReader, so you can pipe incoming binary directly into buffers—see How can I upload binary files? or How can I process multipart HTTP uploads in workflows? for ingest patterns.

Creating and combining buffers

// UTF-8 from string
let text = "Hello opscotch";
let textBuf = byteContext.createFromString(text);

// From bytes
let header = byteContext.createFromByteArray([0xAA, 0xBB]);

// Concatenate
let packet = byteContext.concat([header, textBuf]);

Writing and reading bytes

let buf = byteContext.create(4);
byteContext.writeByte(buf, 0, 0xDE);
byteContext.writeByte(buf, 1, 0xAD);
byteContext.writeByte(buf, 2, 0xBE);
byteContext.writeByte(buf, 3, 0xEF);

let value = byteContext.readByte(buf, 2); // 0xBE
let size = byteContext.getSize(buf); // 4

Bulk copy between buffers:

let source = byteContext.createFromByteArray([1, 2, 3, 4, 5]);
let dest = byteContext.create(8);
byteContext.writeBytes(dest, 2, source, 1, 3); // writes [2,3,4] into dest starting at offset 2

Converting between text and bytes

// Hex to binary and back
let fromHex = byteContext.hexToBinary("48656C6C6F"); // "Hello"
let hex = byteContext.binaryToHex(fromHex); // "48656C6C6F"

// Base64 to binary and back
let fromB64 = byteContext.base64ToBinary("SGVsbG8=");
let b64 = byteContext.binaryToBase64(fromB64); // "SGVsbG8="

Compression

let textBuf = byteContext.createFromString("repeat me ".repeat(50));
let gz = byteContext.gzip(textBuf);
let restored = byteContext.gunzip(gz);

// ZIP alternative
let zipped = byteContext.zip(textBuf);
let unzipped = byteContext.unzip(zipped);

Working with streams (ByteReader)

ByteReader lets you read sequentially without managing offsets. You can get one from a buffer or from the context stream (e.g., HTTP binary upload).

// From an incoming binary HTTP body
let incoming = context.getStream(); // ByteReader from request body
let available = incoming.available();
let bodyBuf = incoming.readAll(); // buffer handle with all bytes

// Inspect as hex for debugging
let fingerprint = byteContext.binaryToHex(bodyBuf).substring(0, 16);

// Sequential parse from a buffer
let reader = byteContext.reader(bodyBuf);
let magic = reader.read(2); // first 2 bytes
let payload = reader.readAll(); // rest

Putting it together

// Receive a binary upload, compress it, and log a short fingerprint
let upload = context.getStream(); // ByteReader
let rawBuf = upload.readAll(); // buffer handle
let gzBuf = byteContext.gzip(rawBuf); // compressed buffer
let hexSig = byteContext.binaryToHex(gzBuf).substring(0, 12);

context.setMessage("stored compressed upload; sig=" + hexSig);

That’s the core: low-level byte primitives, conversions, compression, and streaming, all via handles owned by the agent. Build your higher-level formats on top, and let opscotch manage the buffers for you.

If you need to sign or encrypt what you build, pair these buffers with How can I use industry standard cryptographic functions in workflows.