sinq Lua API & Type Translation
Because sinq bridges a Go runtime with a Lua Virtual Machine, data must be translated back and forth across the boundary. sinq provides a sandboxed Lua environment to give you scriptable control over the HTTP request lifecycle.
To ensure thread safety and prevent side-effect leaks between concurrent scenarios, the API is strictly scoped. Certain functions are only available during specific lifecycle hooks.
1. Global State & Environment
These variables and functions are available globally in all script blocks.
env
A table containing the environment variables configured for the current scenario merged with all the values passed via the -e / --env flags. Modifications made to this table from a user script persist for the lifetime of the scenario.
secrets
A table containing sensitive values passed to sinq via the -s / --secret / --secrets-file CLI flags.
req and res (Current Request Context)
Shorthands for the current request and response being processed.
* req: Used in $PRE to modify the outgoing request (e.g., req.attach()).
* res: A direct reference to sinq.responses[%current%].
Flow Control
sinq.setNextRequest(index): Alters execution flow. The next request executed will be the one at the specifiedindex(1-based). Useful for building loops or conditional skips within a scenario.sinq.finishScenario(): Alters execution flow. Tellssinqto finish the scenario once the current request completes its life cycle. Useful for gracefully finishing loops or conditional scenario shutdowns.
2. Variable Scoping (Local vs. Global)
To prevent cache poisoning and unintended side effects, you should strictly control your variable scoping.
- Temporary Math & Logic: Use the
localkeyword. This ensures the variable is garbage-collected immediately after the script block finishes. - Passing State Across Files: If you need a value from
01_login.sinqto be accessible in02_action.sinq, you must declare it globally (withoutlocal). It will be attached to the Lua sandbox for the lifespan of that specific scenario (_G).
3. Inline Scripts (Request Templating)
Aside from lifecycle hooks, you can use General/Inline scripts to dynamically build your HTTP requests. These are evaluated after $PRE but before the request is sent. The return value of these scripts is injected directly into the raw HTTP text.
You can name them (e.g., $MY_SCRIPT{...}) or leave them anonymous (${...}).
Single-line Interpolation:
If an inline script fails to compile, sinq automatically prepends return and retries. This allows for clean, single-line variable interpolation:
Multi-line Dynamic Generation: For complex logic, use explicit returns:
POST ${env.BASE_URL}/users
Content-Type: application/json
{
"email": "$GENERATE_EMAIL{
local random_num = math.random(1000, 9999)
return 'testuser_' .. random_num .. '@example.com'
}",
"role": "admin"
}
4. Lifecycle-Specific APIs
The following APIs are dynamically injected and destroyed depending on the execution phase of the request.
$PRE (Setup Phase)
Executes before the HTTP request is materialized. Used for file I/O operations.
* req.attach(filepath): Replaces the request body with the contents of the specified file. Note: Fails if a textual body is already defined in the .sinq file.
* req.saveResponseTo(filepath): Streams the upcoming response body directly to disk, bypassing the Lua memory buffer. Ideal for downloading large files. Automatically creates missing directories in the filepath. If used, bodyRaw and JSON methods will not be available in subsequent hooks.
* req.cache(bool?): Turns on/off client-side request caching. The cache is based on the data sent over the wire and any attached filenames (attach, saveResponseTo). The parameter defaults to true if omitted.
* req.skip(bool?): Marks the request to be skipped. Parameter defaults to true if omitted. The $PRE script will finish executing, but the HTTP request will not be fired and subsequent hooks are bypassed. The request is marked as Aborted in the reporter without throwing a test failure.
Both of the file functions expect the path to be relative to the current file. Passing in an absolute path will fail
$RETRY (Polling Phase)
Executes after receiving a response. The script must return a number indicating how many milliseconds to wait before retrying, or a negative number to stop.
sinq.retry.stop: A constant (-1) indicating the retry loop should break immediately.sinq.retry.when(condition, delay?)- Retries if
conditionis true.delaydefaults to500ms. sinq.retry.whenExponential(condition, base?, constant?)- Retries if
conditionis true, using exponential backoff (base ^ attempt * constant). basedefaults to2(Max10).constantdefaults to500ms.sinq.retry.withJitter(condition, range?, delegate?, delegate_args...)- Adds randomized jitter to a retry calculation to prevent thundering herd problems.
rangedefaults to50(±50ms jitter).delegatedefaults tosinq.retry.when, delegate will be passed condition and delegate_args when called.- Usage is:
sinq.retry.withJitter(res.code ~= 200, 100, sinq.retry.when, 2 * sinq.second)- jitter conditional retry with range of [-100:100]
$ASSERT (Validation Phase)
Executes after the retry loop finishes. Used to validate the final state of the response.
sinq.assert.fail(reason): Marks the test as failed with the provided reason. Note: This does not halt Lua execution. The rest of the$ASSERTblock will continue to run, allowing you to collect multiple failure reasons for a single request.sinq.assert.code(expectedHttpCode, message?): Fails if the actual status code does not match.sinq.assert.equals(actual, expected): Fails ifactualdoes not equalexpected. When comparing tables, checks that every key-value pair inexpectedrecursively matches those inactual, but ignores pairs fromactualnot present inexpected.sinq.assert.contains(string, substring, message?): Fails if the string does not contain the specified substring.sinq.assert.isTrue(condition, message?): Fails if the condition resolves tofalseornil.sinq.assert.fileMatches(filepath): Fails if the response previously saved usingreq.saveResponseTo()does not exactly match the contents offilepath. Fails immediately ifreq.saveResponseTo()was not called.
$POST (State Extraction Phase)
Executes after a successful $ASSERT phase. Typically used to parse the final response payload and store relevant data in the global sandbox for subsequent requests. No special scoped APIs are injected here.
5. The Responses Table (sinq.responses)
When an HTTP request completes, sinq parses the response and injects it into the sinq.responses table at the index corresponding to the request number. Lua is 1-indexed, meaning the response to the first request in your scenario is accessed via sinq.responses[1].
Note: A response object only exists after the request has been executed. Accessing
sinq.responses[2]or the aliasresduring the$PREhook of the second request will return nil.
Response Object Structure
attempt(number): The current execution attempt (useful during$RETRY).code(number): The HTTP status code (e.g.,200,404).oversized(boolean | nil):trueif the payload exceeded the scenario'smax_bodylimit and was safely truncated.
Body Access Methods
Note: These are only available if req.saveResponseTo() was NOT used in the $PRE hook.
bodyRaw(string): The raw string of the response payload.extractBodyJson()(function): Safely attempts to parsebodyRawinto a Lua table.- Returns:
(table, error) json()(function): An unsafe convenience wrapper aroundextractBodyJson.- Returns:
tabledirectly. - Throws: Calls a fatal
error()if the body is not valid JSON, failing the scenario immediately.
HTTP Headers Translation
HTTP headers are complex because a single key can have multiple values. sinq handles this translation automatically.
- Single Value Headers: Translated to a standard Lua string.
- Multi-Value Headers: Translated to a 1-indexed Lua table (array) of strings.
The JSON Blindspot (1-Indexed Arrays)
In Go and in general, arrays are 0-indexed. In Lua, tables are 1-indexed.
If your API returns a top-level JSON array, sinq translates it into a Lua table starting at index 1.
API Response:
Lua Assertion:
$ASSERT{
-- Correct: Access the first element at index 1
local data = res.json()
local first_id = data[1].id
if first_id ~= 42 then
sinq.assert.fail("ID mismatch")
end
}
6. Extensions Quick Reference
sinq.time.ms,sinq.time.second,sinq.time.minute,sinq.time.hoursinq.time.now()sinq.time.fromString(str, format?)sinq.time.toString(ms, format?)sinq.crypto.base64Encode(string)sinq.crypto.base64Decode(string)sinq.crypto.base64UrlEncode(string)sinq.crypto.base64UrlDecode(string)sinq.crypto.hexEncode(string)sinq.crypto.hexDecode(string)sinq.crypto.md5(string, encoding?)sinq.crypto.sha1(string, encoding?)sinq.crypto.sha256(string, encoding?)sinq.crypto.sha512(string, encoding?)sinq.crypto.hmac(source, algo?, key?, encoding?)sinq.jwt.decode(token)sinq.jwt.verify(token, key, algo?)sinq.jwt.sign(claimsTable, key, method?)sinq.json.parse(string)sinq.json.serialize(table, indent?)
7. Time API (sinq.time.*)
Built-in constants and functions to make time-based logic and parsing possible.
Constants
sinq.time.ms(1)sinq.time.second(1000)sinq.time.minute(60000)sinq.time.hour(3600000)
Note: Lua uses
float64for numbers. When converting a timestamp from milliseconds to another unit (e.g., seconds) using division, usemath.floorto ensure a clean integer:math.floor(sinq.time.now() / sinq.time.second).
Functions
sinq.time.now(): Returns the current UNIX timestamp.- Returns:
number(milliseconds since epoch). sinq.time.fromString(str, format?): Parses a time string into a UNIX timestamp (milliseconds). Theformatstring is optional.- Returns:
(number, error) - Format Rules: Uses Go's time layout rules. If omitted, defaults to ISO8601 (
2006-01-02T15:04:05.000Z07:00). sinq.time.toString(ms, format?): Formats a UNIX timestamp (milliseconds) into a time string. Theformatstring is optional.- Returns:
string - Format Rules: Uses Go's time layout rules. If omitted, defaults to ISO8601.
8. Crypto API (sinq.crypto.*)
Provides standard cryptographic encoding and hashing functions.
Encoding
sinq.crypto.base64Encode(string): Encodes a string into standard Base64.- Returns:
string sinq.crypto.base64Decode(string): Decodes a standard Base64 string.- Returns:
(string, error) sinq.crypto.base64UrlEncode(string): Encodes a string into URL-safe Base64.- Returns:
string sinq.crypto.base64UrlDecode(string): Decodes a URL-safe Base64 string.- Returns:
(string, error) sinq.crypto.hexEncode(string): Encodes a string into a hexadecimal representation.- Returns:
string sinq.crypto.hexDecode(string): Decodes a hexadecimal string.- Returns:
(string, error)
Hashing
sinq.crypto.md5(string, encoding?),sinq.crypto.sha1(string, encoding?),sinq.crypto.sha256(string, encoding?),sinq.crypto.sha512(string, encoding?): Computes the cryptographic hash of the input string.- Returns:
(string, error) - Parameters:
encodingdefaults to"hex". Supported values are"hex","base64","base64url", and"raw". - Note: Since it defaults to
"hex", the output is safe to print and transmit. If"raw"is used, the function returns the raw bytes. - Throws: Returns an error string as the second value if an unknown encoding string is provided.
sinq.crypto.hmac(source, algo?, key?, encoding?): Computes the HMAC of the source string.- Returns:
(string, error) - Parameters:
algodefaults to"sha256". Supported values are"sha256","sha1","sha512", and"md5".keydefaults to"".encodingdefaults to"hex". Supported values are"hex","base64","base64url", and"raw". - Throws: Returns an error string as the second value if an unknown algorithm or encoding string is provided.
9. JWT API (sinq.jwt.*)
Allows for generation, decoding, and validation of JSON Web Tokens natively within your scenario flow.
sinq.jwt.decode(token): Decodes a JWT token without validating its signature.- Returns:
(table, error) - Table Structure: Contains
header(table),claims(table),signature(string), andmethod(string). sinq.jwt.verify(token, key, algo?): Verifies the token using the provided key and optional algorithm constraint.- Returns:
(table, error)Note: Symmetric algorithms (
HS*) use raw string keys. Asymmetric algorithms (RS*,ES*,EdDSA) require PEM-encoded public keys. sinq.jwt.sign(claimsTable, key, method?): Creates a signed JWT string. Returnsstring, error.claimsTable: A Lua table representing the JWT payload.key: The signing key string.method?: The signing algorithm. Defaults toHS256.Note: The
claimsTablemust have strictly string keys. Mixing list-style (integer) indices with string keys in Lua will cause parsing to fail and return an error. Asymmetric algorithms require PEM-encoded private keys. Warning: Passing a cyclic table as theclaimsTablewill result in a serialization error being returned as a second return value (nil, "Failed to serialize..."). It is safe and will not crash the runner, but the token will not be generated.
10. JSON Utilities (sinq.json.*)
The sinq.json table provides explicit methods to parse and serialize JSON data from Lua.
sinq.json.parse(string): Parses a JSON string into a Lua table. Returnstable, error.sinq.json.serialize(table, indent?): Serializes a Lua table into a JSON string. Returnsstring, error.indent?: Optional string used for formatting (e.g.," "). If omitted, produces compact JSON. If present, also introduces newlines between object and array entries.Note: Passing a cyclic table will immediately return an error (
"Cycle detected, unable to serialize").sinq.json.null: A special constant representing a JSONnullvalue, allowing Lua tables to explicitly serializenullproperties (since standard Lua dropsniltable keys). Tables, parsed from JSON will also include this constant to represent explicitnull. Can be compared with standard==operator (sinq.assert.isTrue(res.json().myNull == sinq.json.null))
11. Fake Data Generation (sinq.fake.*)
The sinq.fake table exposes deterministic fake data generators. All generators respect the current seed.
Primitives & Core Data
sinq.fake.uuid(): Returns a random UUIDv4 string.sinq.fake.int(min?, max?): Returns a random integer.sinq.fake.float(min?, max?): Returns a random float.sinq.fake.shakespeare(): Returns a random boolean (trueorfalse).sinq.fake.oneOf(array): Accepts a Lua array (table with integer keys) and returns a random element.
Networking & Web
sinq.fake.email(): Returns a random email address.sinq.fake.ipv4(): Returns a random IPv4 address.sinq.fake.ipv6(): Returns a random IPv6 address.sinq.fake.url(): Returns a random URL string.sinq.fake.userAgent(): Returns a random User-Agent string.sinq.fake.trace(): Returns a random W3C traceparent header string.sinq.fake.username(): Returns a random username.sinq.fake.password(): Returns a random password.
Identity & Text
sinq.fake.name(): Returns a full name.sinq.fake.firstName(): Returns a first name.sinq.fake.lastName(): Returns a last name.sinq.fake.phone(): Returns a random phone number.sinq.fake.address(): Returns a full address.sinq.fake.company(): Returns a company name.sinq.fake.word(): Returns a single random word.
Time & Configuration
sinq.fake.timestamp(fromMs, toMs?): Returns a random UNIX timestamp (integer milliseconds) betweenfromMsandtoMs. IftoMsis omitted, it defaults to the current time.sinq.fake.setSeed(int64): Seeds the fake data generator to ensure deterministic output across runs.
Additional Randomness
math.random(max?),math.random(min, max): Lua's standard way of generating pseudo-random data is present insinqand always available.
12. Libraries
sinq does not load two of common core Lua libraries - io and os by default. This is done in order to prevent .sinq scripts from becoming a safety concern when run without due diligence. To enable these libraries in Lua scripts use --unrestricted flag, and only run trusted scripts with this flag.
sinq allows users to import external Lua packages. For them to be accessible via the require("package") calls, path to the directory containing the files for the package should be passed to sinq via the environment variable SINQ_LUA_PATH or via the --plugins flag. If both present, the flag takes precedence. The path is expected to consist of plain paths to the directories joined with : on Linux and MacOS, ; on Windows. Several --plugins flags can be passed on startup, which will result in an aggregated list of all paths within them.
Note: All paths passed to
sinqas positional arguments and the current working directory also get appended to the end of path for the purposes of searching for Lua plugins. So if you runsinqfrom a directory containing a filemy-module.lua,require("my-module")will work for all.sinqfiles.