AFScript
What is AFScript?
AFScript is an interpreted programming language created by AttackForge.
It was built to help our customers to configure and personalize their AttackForge application to better match their requirements and needs.
Applications of AFScript could include:
Changing the logic in parts of the application to align with existing or intended workflows
Drive behaviour of forms and their respective fields
Create in-app automations
Apply pre-and-post data transformations
Build bespoke dashboards and analytics
AFScript is the next generation in empowerment for AttackForge customers. It comes off the back of the successes we’ve had with Hide Expressions (custom fields and sections, vulnerability SLAs, custom vulnerability parsing) and Filter Expressions (custom emails, APIs) which have provided customers with ways to make AttackForge their own.
We built AFScript to provide a safe and secure path for our customers to apply their own code to their AttackForge application, in a performant manner and without creating any security holes.
The language was built to look and feel like JavaScript to make it familiar and easy to use for security teams, pentesters and software engineers.
Importantly, the language itself is not executable. This makes it safe to use in a secure way. AttackForge will interpret AFScript and derive actions to take, without executing arbitrary code. This important distinction is why we had to build our own programming language instead of going with many of the existing languages already available.
How does AFScript work?
For the most part, write AFScript the same way you would write JavaScript code.
If you are not familiar with JavaScript, we recommend checking the JavaScript basics guide by Mozilla.
We built AFScript to resemble the primary syntax of JavaScript. You can define and use variables, create loops, create functions, call functions inside functions, etc.
To make the language easier to use, we have included built-in Functions which resemble common JavaScript built-in objects like Math, String and others.
We’ve also added our own functions and syntax which was inspired by various other programming languages, such as Date.datetime.
What can’t AFScript do?
For mostly security reasons, there are some limitations in AFScript that could otherwise be found in JavaScript. This is by design, to prevent accidental or intentional security holes.
The following is a non-exhaustive list of limitations. We will continue to update this list over time as the language evolves.
No support for anonymous functions
No support for creating Classes
No support for try…catch
No support for Symbol
No support for new operator
No support for this
Writing AFScript
AFScript is a sandboxed subset of JavaScript. A script is a program: top-level statements run in order, and whatever it returns is the result. It gets let/const, functions, closures, arrows, template strings, destructuring, spread and a curated standard library. It gets no path into the host runtime — no require, no eval, no Function, no timers, no network, no filesystem, no console, and no reachable JavaScript prototypes.
If you already know JavaScript you know 90% of AFScript. The remaining 10% is where every mistake lives, so read the next two sections before writing a line.
The one rule: no method call syntax
a.b is a property read, never a method lookup. There is no a.b() call form for anything the value did not itself put there.
Reading a name the value owns works exactly as in JavaScript:
Reading a name that only a prototype would have supplied throws. It does not return undefined — it ends the script:
length is the one exception worth knowing: it is a genuine own property of an array and of a string, so items.length and s.length do read. Use Array.length(items) and String.length(s) anyway — they are the documented API, and they check their receiver, where .length silently answers undefined on anything that is not an array or a string.
Built-ins live on namespace objects, and the receiver is the first argument:
The mechanical rule. Before you write a dot, ask what is on the left.
About to write
x.foo(a, b)→ writeNamespace.foo(x, a, b).About to write
x.length→ writeArray.length(x)orString.length(x).Left side is a value you built (an object literal, a host global, a parsed JSON result) and the name is one you put there → the dot is correct, leave it.
The ten namespaces are Array, String, Object, JSON, XML, Date, Math, Number, Util, Logger. All ten are const — readable, never replaceable. Infinity and NaN are the only bare globals the language defines.
JavaScript → AFScript
xs.length
Array.length(xs) — .length does read, but is unchecked; prefer this
s.length
String.length(s)
xs.map(f) · .filter · .reduce · .find · .some · .every · .sort · .join · .includes · .slice · .push
Array.map(xs, f), Array.filter(xs, f), …
s.trim() · .split · .replace · .toUpperCase · .startsWith · .padStart
String.trim(s), String.split(s, ','), …
'' + n or `${n}` used as a coercion
String.from(n) — String.concat refuses a non-string
Object.keys(o) · entries · values
same — but Object.keys('abc') throws
Object.assign · freeze · fromEntries · hasOwn
absent — use { ...a, ...b } in place of assign
JSON.parse · JSON.stringify
same — JSON.parse answers undefined on bad input, it does not throw
new Date() · d.toISOString()
Date.datetime('now') — there is no Date value type
/re/.test(s)
s =~ m/re/
s.match(/re/g)
String.match(s, m/re/g) — answers a plain array of strings, or null
let a = 1, b = 2;
two statements — one binding per declaration
x => ({ a: 1 })
same — { after => starts a block, so an object literal must be parenthesised
({ a }) => a
same — a destructuring arrow parameter must be parenthesised
[...a, ...b] · { ...o } · f(...args)
same — spread works in all three places
for (const x of xs)
same — but only over an array or a string
'k' in obj
obj.k !== undefined — there is no in operator
console.log(x)
Logger.info(x) — wrap objects in JSON.stringify first
try { … } catch { … }
no equivalent — validate before you act
class · new · async · await · yield · var · instanceof · void
no equivalent
What the language has
Present:
let/constand block scoping;hoisted function declarations;
arrow functions;
real closures;
parameter defaults;
destructuring in declarations, assignment, parameters and
for…of/for…inheads (with defaults, rest, nesting, renamed and computed keys);spread in array literals, object literals and call arguments;
shorthand properties;
template strings with
${};if/else,while,do…while, C-stylefor,for…of,for…in,switchwith fall-through,break,continue,return;arithmetic including
**, bitwise ops, shifts, comparison,===/!==,&&,||,??, ternary,typeof,delete, pre/post++/--, compound assignment (+=,??=, …);optional access
a?.banda?[k](no dot before the bracket);regex literals
m/pattern/flagsmatched with=~;numbers in decimal, hex, octal and binary, plus their BigInt forms;
//and/* */comments;
Absent:
class/extends/super;new;try/catch/finally/throw;async/awaitand promises;generators and
yield;var;this;arguments;instanceof;inas a binary operator;void;the comma operator and multi-declarator
let a = 1, b = 2(so nofor (let i = 0, j = 0; …)either);labelled statements and
break label;tagged templates and
String.raw;/re/literal syntax;optional call
a?.();getters, setters and object method shorthand
{ foo() {} };a trailing comma in a parameter list (allowed in call arguments, literals and patterns);
iterators and
Symbol.iterator;import/export/require;eval;Function;timers;
I/O;
console;Map,Set,Symbol,Promise, typed arrays;RegExp,DateandErrorobject types;
Values are checked, never coerced
Almost every built-in validates what it is handed and throws rather than guessing. This is the second-biggest source of surprise after method calls.
Convert explicitly. String.from is the one member that exists to coerce, and the one that never throws:
The exceptions: Array.isArray, all of Number, and all of Math except secureRandom and secureRandomInt are host intrinsics passed straight through, so they coerce exactly as JavaScript does.
Examples
for…of and for…in
for…of walks an array's elements or a string's characters, and throws on anything else. Strings are walked by code point, so an astral character is one iteration rather than a surrogate pair.
for…in walks own enumerable keys as strings — an object's names, an array's or string's index names, and nothing at all for null, undefined or a primitive.
for…of gives the loop variable a fresh binding each iteration, so a function made in the body captures that iteration's value. A C-style for header keeps its single binding. Both match JavaScript, and the difference is worth knowing:
Arrow functions
An expression that evaluates to a function, so a callback no longer has to be hoisted out into a named declaration.
An expression body is the result outright; a block body needs return. x => {} is an empty block that answers undefined, not an object literal. function f() {} — an empty body on a declaration — is now legal too.
Template string literals
Interpolations hold any expression, nest, and coerce their value exactly as + would. Escapes \`` and $` are available for a literal backtick or dollar. Tagged templates are not included.
Switch
=== matching, fall-through until a break, and default honoured wherever it sits in the clause list. The whole body shares one block scope.
Spread and shorthand properties
Spread works in the three places JavaScript puts it — array literals, object literals and call arguments — and object literals gained { x, y } shorthand.
What may be spread is decided by type, not by asking the value for an iterator: arrays and strings for the array and argument forms, own enumerable keys for the object form. A function value cannot be spread at all.
Logging
Logging is imperative when writing any script or code.
AFScript supports logging to help you debug your scripts and logic.
You can invoke a log message using the following syntax:
Logger.<type>(“message”);
You can also include multiple messages in the same log entry using a comma to separate each message:
Logger.<type>(“message1”,“message2”,“message3”);
Where <type> is one of the following log level types:
fatal
error
warn
info
debug
trace
Fatal
Logger.fatal(“a fatal error occurred in xyz”);
Error
Logger.error(“an error occurred in xyz”);
Warn
Logger.warn(“a warning for xyz”);
Info
Logger.info(“an informational message for xyz”);
Debug
Logger.debug(“a debug statement”);
Trace
Logger.trace(“function abc was called”);
Built-in Functions
Dates
datetime
The Date.datetime(“timeValue”, “modifiers”, “isostring”, “epoch”) function can be used to construct a date and time.
You can modify the date and time by passing modifiers.
The default response is the date & time in an ISO string format.
However, you can set the response to be in epoch format by passing in “epoch”.
Similarly, you can explicitly set the response to an ISO string format by passing in “isostring”.
timeValue - must be either:
“now”
“YYYY-MM-DD”
“YYYY-MM-DDTHH:MM”
“YYYY-MM-DDTHH:MM:SS”
“YYYY-MM-DDTHH:MM:SS:MMMZ”
time in milliseconds e.g. "1727246762913"
modifiers - must be either:
“+999 years”
“-999 years”
“+999 months”
“-999 months”
“+999 days”
“-999 days”
“+999 hours”
“-999 hours”
“+999 minutes”
“-999 minutes”
“start of year”
“start of month”
“start of day”
format
The Date.format(“timeValue”, “mask”, "timezone") function can be used to convert a date and time to a specified mask and optional timezone.
timeValue - must be either:
“now”
“YYYY-MM-DD”
“YYYY-MM-DDTHH:MM”
“YYYY-MM-DDTHH:MM:SS”
“YYYY-MM-DDTHH:MM:SS:MMMZ”
time in milliseconds e.g. "1727246762913"
mask - must be either:
Named formats
defaultddd mmm dd yyyy HH:MM:ss
Sat Jun 09 2007 17:46:21
shortDatem/d/yy
6/9/07
paddedShortDatemm/dd/yyyy
06/09/2007
mediumDatemmm d, yyyy
Jun 9, 2007
longDatemmmm d, yyyy
June 9, 2007
fullDatedddd, mmmm d, yyyy
Saturday, June 9, 2007
shortTimeh:MM TT
5:46 PM
mediumTimeh:MM:ss TT
5:46:21 PM
longTimeh:MM:ss TT Z
5:46:21 PM EST
isoDateyyyy-mm-dd
2007-06-09
isoTimeHH:MM:ss
17:46:21
isoDateTimeyyyy-mm-dd'T'HH:MM:sso
2007-06-09T17:46:21+0700
isoUtcDateTimeUTC:yyyy-mm-dd'T'HH:MM:ss'Z'
2007-06-09T22:46:21Z
Mask options
dDay of the month as digits; no leading zero for single-digit days.
ddDay of the month as digits; leading zero for single-digit days.
dddDay of the week as a three-letter abbreviation.
DDD"Ysd", "Tdy" or "Tmw" if date lies within these three days. Else fall back to ddd.
ddddDay of the week as its full name.
DDDD"Yesterday", "Today" or "Tomorrow" if date lies within these three days. Else fall back to dddd.
mMonth as digits; no leading zero for single-digit months.
mmMonth as digits; leading zero for single-digit months.
mmmMonth as a three-letter abbreviation.
mmmmMonth as its full name.
yyYear as last two digits; leading zero for years less than 10.
yyyyYear represented by four digits.
hHours; no leading zero for single-digit hours (12-hour clock).
hhHours; leading zero for single-digit hours (12-hour clock).
HHours; no leading zero for single-digit hours (24-hour clock).
HHHours; leading zero for single-digit hours (24-hour clock).
MMinutes; no leading zero for single-digit minutes.
MMMinutes; leading zero for single-digit minutes.
NISO 8601 numeric representation of the day of the week.
oGMT/UTC timezone offset, e.g. -0500 or +0230.
pGMT/UTC timezone offset, e.g. -05:00 or +02:30.0
sSeconds; no leading zero for single-digit seconds.
ssSeconds; leading zero for single-digit seconds.
SThe date's ordinal suffix (st, nd, rd, or th). Works well with
d.
lMilliseconds; gives 3 digits.
LMilliseconds; gives 2 digits.
tLowercase, single-character time marker string: a or p.
ttLowercase, two-character time marker string: am or pm.
TUppercase, single-character time marker string: A or P.
TTUppercase, two-character time marker string: AM or PM.
WISO 8601 week number of the year, e.g. 4, 42
WWISO 8601 week number of the year, leading zero for single-digit, e.g. 04, 42
ZUS timezone abbreviation, e.g. EST or MDT. For non-US timezones, the GMT/UTC offset is returned, e.g. GMT-0500
'...',"..."Literal character sequence. Surrounding quotes are removed.
UTCMust be the first four characters of the mask. Converts the date from local time to UTC/GMT/Zulu time before applying the mask. The "UTC:" prefix is removed.
timezone - must be a canonical timezone
Strings
The String.at() method takes an integer value and returns a new String consisting of the single UTF-16 code unit located at the specified offset. This method allows for positive and negative integers. Negative integers count back from the last string character.
The String.charAt() method returns a new string consisting of the single UTF-16 code unit at the given index.
String.charAt() always indexes the string as a sequence of UTF-16 code units, so it may return lone surrogates.
The String.concat() method concatenates the string arguments to this string and returns a new string.
decode
digest
encode
The String.endsWith() method determines whether a string ends with the characters of this string, returning true or false as appropriate.
from
The String.from() method returns a string representing the primitive or object.
hmac
The String.includes() method performs a case-sensitive search to determine whether a given string may be found within this string, returning true or false as appropriate.
The String.indexOf() method searches this string and returns the index of the first occurrence of the specified substring. It takes an optional starting position and returns the first occurrence of the specified substring at an index greater than or equal to the specified number.
The String.lastIndexOf() method searches this string and returns the index of the last occurrence of the specified substring. It takes an optional starting position and returns the last occurrence of the specified substring at an index less than or equal to the specified number.
The String.length data property of a String value contains the length of the string in UTF-16 code units.
The String.match() method of String values retrieves the result of matching this string against a regular expression.
The String.matchAll() method of String values returns an iterator of all results matching this string against a regular expression, including capturing groups.
The String.padEnd() method pads this string with a given string (repeated, if needed) so that the resulting string reaches a given length. The padding is applied from the end of this string.
The String.padStart() method pads this string with another string (multiple times, if needed) until the resulting string reaches the given length. The padding is applied from the start of this string.
The String.repeat() method constructs and returns a new string which contains the specified number of copies of this string, concatenated together.
The String.replace() method returns a new string with one, some, or all matches of a pattern replaced by a replacement. The pattern can be a string or a RegExp, and the replacement can be a string or a function called for each match. If pattern is a string, only the first occurrence will be replaced. The original string is left unchanged.
The String.replaceAll() method returns a new string with all matches of a pattern replaced by a replacement. The pattern can be a string or a RegExp, and the replacement can be a string or a function to be called for each match. The original string is left unchanged.
The String.search() method executes a search for a match between a regular expression and this string, returning the index of the first match in the string.
sign / verify
The String.sign() method is used to cryptographically sign a message using a private key for signing, which can be later verified with a public key using the String.verify() method.
The following hashing algorithms are supported:
The following encodings are supported:
The String.slice() method extracts a section of this string and returns it as a new string, without modifying the original string.
The String.split() method takes a pattern and divides this string into an ordered list of substrings by searching for the pattern, puts these substrings into an array, and returns the array.
The String.startsWith() method determines whether this string begins with the characters of a specified string, returning true or false as appropriate.
The String.substring() method returns the part of this string from the start index up to and excluding the end index, or to the end of the string if no end index is supplied.
The String.trim() method removes whitespace from both ends of this string and returns a new string, without modifying the original string.
To return a new string with whitespace trimmed from just one end, use trimStart() or trimEnd().
The String.trimEnd() method removes whitespace from the end of this string and returns a new string, without modifying the original string.
The String.trimStart() method removes whitespace from the beginning of this string and returns a new string, without modifying the original string.
Arrays
The Array.at() method takes an integer value and returns the item at that index, allowing for positive and negative integers. Negative integers count back from the last item in the array.
The Array.concat() method is used to merge two or more arrays. This method does not change the existing arrays, but instead returns a new array.
The Array.entries() method returns a new array iterator object that contains the key/value pairs for each index in the array.
The Array.every() method tests whether all elements in the array pass the test implemented by the provided function. It returns a Boolean value.
The Array.fill() method changes all elements within a range of indices in an array to a static value. It returns the modified array.
The Array.filter() method creates a shallow copy of a portion of a given array, filtered down to just the elements from the given array that pass the test implemented by the provided function.
The Array.find() method returns the first element in the provided array that satisfies the provided testing function. If no values satisfy the testing function, undefined is returned.
If you need the index of the found element in the array, use findIndex().
If you need to find the index of a value, use indexOf(). (It's similar to findIndex(), but checks each element for equality with the value instead of using a testing function.)
If you need to find if a value exists in an array, use includes(). Again, it checks each element for equality with the value instead of using a testing function.
If you need to find if any element satisfies the provided testing function, use some().
The Array.findIndex() method returns the index of the first element in an array that satisfies the provided testing function. If no elements satisfy the testing function, -1 is returned.
See also the find() method, which returns the first element that satisfies the testing function (rather than its index).
The Array.findLast() method iterates the array in reverse order and returns the value of the first element that satisfies the provided testing function. If no elements satisfy the testing function, undefined is returned.
The Array.findLastIndex() method iterates the array in reverse order and returns the index of the first element that satisfies the provided testing function. If no elements satisfy the testing function, -1 is returned.
The Array.flat() method creates a new array with all sub-array elements concatenated into it recursively up to the specified depth.
The Array.flatMap() method returns a new array formed by applying a given callback function to each element of the array, and then flattening the result by one level.
The Array.from() static method creates a new, shallow-copied Array instance from an iterable or array-like object.
The Array.includes() method determines whether an array includes a certain value among its entries, returning true or false as appropriate.
The Array.indexOf() method returns the first index at which a given element can be found in the array, or -1 if it is not present.
The Array.join() method creates and returns a new string by concatenating all of the elements in this array, separated by commas or a specified separator string. If the array has only one item, then that item will be returned without using the separator.
The Array.keys() method returns a new array iterator object that contains the keys for each index in the array.
The Array.lastIndexOf() method returns the last index at which a given element can be found in the array, or -1 if it is not present. The array is searched backwards, starting at fromIndex.
The Array.length data property represents the number of elements in that array. The value is an unsigned, 32-bit integer that is always numerically greater than the highest index in the array.
The Array.map() method creates a new array populated with the results of calling a provided function on every element in the calling array.
The Array.of() static method creates a new Array instance from a variable number of arguments, regardless of number or type of the arguments.
The Array.pop() method removes the last element from an array and returns that element. This method changes the length of the array.
The Array.push() method adds the specified elements to the end of an array and returns the new length of the array.
The Array.reduce() method executes a user-supplied "reducer" callback function on each element of the array, in order, passing in the return value from the calculation on the preceding element. The final result of running the reducer across all elements of the array is a single value.
The first time that the callback is run there is no "return value of the previous calculation". If supplied, an initial value may be used in its place. Otherwise the array element at index 0 is used as the initial value and iteration starts from the next element (index 1 instead of index 0).
The Array.reduceRight() method applies a function against an accumulator and each value of the array (from right-to-left) to reduce it to a single value.
The Array.reverse() method reverses an array in place and returns the reference to the same array, the first array element now becoming the last, and the last array element becoming the first. In other words, elements order in the array will be turned towards the direction opposite to that previously stated.
The Array.shift() method removes the first element from an array and returns that removed element. This method changes the length of the array.
The Array.slice() method returns a shallow copy of a portion of an array into a new array object selected from start to end (end not included) where start and end represent the index of items in that array. The original array will not be modified.
The Array.some() method tests whether at least one element in the array passes the test implemented by the provided function. It returns true if, in the array, it finds an element for which the provided function returns true; otherwise it returns false. It doesn't modify the array.
The Array.sort() method sorts the elements of an array in place and returns the reference to the same array, now sorted. The default sort order is ascending, built upon converting the elements into strings, then comparing their sequences of UTF-16 code units values.
The Array.unshift() method adds the specified elements to the beginning of an array and returns the new length of the array.
The Array.values() method returns a new array iterator object that iterates the value of each item in the array.
Objects
The Object.entries() static method returns an array of a given object's own enumerable string-keyed property key-value pairs.
The Object.keys() static method returns an array of a given object's own enumerable string-keyed property names.
The Object.values() static method returns an array of a given object's own enumerable string-keyed property values.
JSON
The JSON.parse() static method parses a JSON string, constructing the JavaScript value or object described by the string.
The JSON.stringify() static method converts a JavaScript value to a JSON string, optionally replacing values if a replacer function is specified or optionally including only the specified properties if a replacer array is specified.
XML
The XML.parse() static method parses a XML string, constructing the JSON value or object described by the string.
Math
The Math.atan2() static method returns the angle in the plane (in radians) between the positive x-axis and the ray from (0, 0) to the point (x, y), for Math.atan2(y, x).
The Math.ceil() static method always rounds up and returns the smallest integer greater than or equal to a given number.
The Math.clz32() static method returns the number of leading zero bits in the 32-bit binary representation of a number.
The Math.floor() static method always rounds down and returns the largest integer less than or equal to a given number.
The Math.fround() static method returns the nearest 32-bit single precision float representation of a number.
The Math.imul() static method returns the result of the C-like 32-bit multiplication of the two parameters.
The Math.log1p() static method returns the natural logarithm (base e) of 1 + x, where x is the argument.
The Math.max() static method returns the largest of the numbers given as input parameters, or -Infinity if there are no parameters.
The Math.min() static method returns the smallest of the numbers given as input parameters, or Infinity if there are no parameters.
The Math.random() static method returns a floating-point, pseudo-random number that's greater than or equal to 0 and less than 1, with approximately uniform distribution over that range — which you can then scale to your desired range. The implementation selects the initial seed to the random number generation algorithm; it cannot be chosen or reset by the user.
secureRandom
The Math.secureRandom() static method returns a floating-point, random number generated using a CSPRNG (Cryptographically Secure Pseudo-Random Number Generator) that's greater than or equal to 0 and less than 1, with approximately uniform distribution over that range — which you can then scale to your desired range. The implementation selects the initial seed to the random number generation algorithm; it cannot be chosen or reset by the user.
secureRandomInt
The Math.secureRandomInt() static method returns an integer in [min, max], inclusive at both ends, random number generated using a CSPRNG (Cryptographically Secure Pseudo-Random Number Generator).
The Math.sign() static method returns 1 or -1, indicating the sign of the number passed as argument. If the input is 0 or -0, it will be returned as-is.
The Math.trunc() static method returns the integer part of a number by removing any fractional digits.
Numbers
The Number.isSafeInteger() static method determines whether the provided value is a number that is a safe integer.
The Number.parseFloat() static method parses an argument and returns a floating point number. If a number cannot be parsed from the argument, it returns NaN.
The Number.parseInt() static method parses a string argument and returns an integer of the specified radix or base.
Util
randomId
The Util.randomId() method returns a cryptographically secure random value, encoded using a supported encoding.
The first parameter must be a string specifying one of the following encodings:
The second parameter must be a number which specifies how many bytes the random value should be.
uuidv4
The Util.uuidv4() method returns a Universally Unique IDentifier (UUID) also known as a GUID (Globally Unique IDentifier).
A UUID is 128 bits long, and can guarantee uniqueness across space and time.
Regular Expressions
You can test with Regular Expressions (RegExp) using the following syntax:
Context
When interacting with AFScript, each supported use will have a context which is automatically injected and made available for you to use.
The context is made up of data which is contextually relevant for the purpose for which you are applying AFScript.
For example, when updating the logic for project status calculations – the context will include data about the project, which can be used in your logic to derive the intended behaviour and result.

When building scripts and debugging, you can update the context to ensure the robustness of your logic and code.

Code Editing
AttackForge has a built-in lightweight code editor to help you to use AFScript.

The editor supports autocomplete which also maps to the context.

You can also easily access Built-in Functions.

Output
You can test the output of your script and logic. This helps to provide assurance that your script is working as expected prior to rolling it into the application.
The output can be viewed at the bottom of the page.

Clicking Run will run your code and show the output.
Clicking Preserve log will preserve logging from multiple runs in the output.
TIP: you can use Logging to help you debug your code.
Supported Use Cases
Workflow Automation and Custom Integrations
You can use AFScript in your Actions using Flows.
Project Status Calculations
You can use AFScript to change the logic for how project status is calculated.
To get started, click on Administration -> Projects -> Status -> Configure

The default calculations for project status are included below.
IMPORTANT: Project statuses relying on Date.datetime() will be automatically updated every 5 minutes - it is not a live calculation. This is to ensure performance, especially when re-calculating all projects and comparing to 'now'.
The return value must be one of the following values:
"On Hold"
"Overrun"
"Retest"
"Completed"
"Testing"
"Waiting to Start"
Your code does not need to handle all statuses above if you do not intend to use all statuses.
Example 1: Project Completed based on Project End Date
This example will automatically show the project as completed if now (the time you are viewing the project status in the application) is any time after the project end date.
NOTE: Overrun status has been removed as it logically does not apply under this example use.
IMPORTANT: Project statuses relying on Date.datetime() will be automatically updated every 5 minutes - it is not a live calculation. This is to ensure performance, especially when re-calculating all projects and comparing to 'now'.
Example 2: Project Completed based on Custom Field
This example will automatically show the project as completed if a project custom field ‘project_completed’ has a value of ‘Yes’.
NOTE: This example requires configuration of a project custom field. For more information on how to do this, see Custom Fields & Forms.
NOTE: Overrun status has been removed as it logically does not apply under this example use.
IMPORTANT: Project statuses relying on Date.datetime() will be automatically updated every 5 minutes - it is not a live calculation. This is to ensure performance, especially when re-calculating all projects and comparing to 'now'.
Example 3: Project Retest based on Retesting Rounds
This example will automatically show the project as retest if there is at least one outstanding retesting round.
IMPORTANT: Project statuses relying on Date.datetime() will be automatically updated every 5 minutes - it is not a live calculation. This is to ensure performance, especially when re-calculating all projects and comparing to 'now'.
Suggested Values
You can use AFScript to suggest values for fields.
Suggestions can help to guide users into completing forms, based on your own logic.
For example:
Suggest a project code or vulnerability code on a project
Suggest a custom score for a vulnerability
Suggest a budget for a project/test, based on how scoping questions have been answered
Suggest missing evidence for a vulnerability
Suggest execution flows for a test case
!IMPORTANT: Suggested values are only supported on the Project form at present. This feature is expected to be widely supported in Q4, 2025.
Suggestion Formats
Input fields
Must return a string, for example:
Text Area fields
Must return a string, for example:
Rich-Text fields
Must return a string. Can be HTML, for example:
Select fields
Must return a string, for example:
Multi-Select fields
Must return a string array, for example:
Date fields
Must return a string in ISO 8601 UTC format (YYYY-MM-DDThh:mm:ssZ), for example:
Table fields
Must return an array of objects. Each object must include the key for the column field and appropriate values, for example:
List fields
Must return a string array, for example:
User Select fields
Must return a string array with each string as an Object Id, for example:
User Multi-Select fields
Must return a string array with each string as an Object Id, for example:
Group Select fields
Must return a string array with each string as an Object Id, for example:
Group Multi-Select fields
Must return a string array with each string as an Object Id, for example:
Project Code and Vulnerability Code
You can suggest a custom project code and a vulnerability code prefix when creating or editing a project.


To get started with Project Code, click on Administration -> Projects -> Fields (Code) -> Suggested Value (Configure)

To get started with Vulnerability Code, click on Administration -> Projects -> Fields (Vulnerability Code)

Example 1: Suggest A Code Based on the Customer
This example will suggest a code which is made up of the customer name (or a short name for the customer you can create a mapping for).
Example input: ACME Corp.
Example project code: ACME
PREREQUSITIES:
You must have a SELECT type project custom field which is used to select the customer on the project. This example uses a custom field with a key 'customer'.

Selecting the customer:

Suggested project code:

Example 2: Suggest A Code Based on the Customer and Testing Types
This example will suggest a code which is made up of the customer name (or a short name for the customer you can create a mapping for) as well as the testing types assigned to the project.
Example input: ACME Corp. + Web App
Example project code: ACME-WEBAPP
PREREQUSITIES:
You must have a SELECT type project custom field which is used to select the customer on the project. This example uses a custom field with a key 'customer'.
You must have a MULTI-SELECT type project custom field which is used to select the testing types assigned to the project. This example uses a custom field with a key 'testing_types'.

Selecting the customer and testing types:

Suggested project code:

Example 3: Suggest A Code Based on the Customer, Testing Types and a Generated Random Number
This example will suggest a code which is made up of the customer name (or a short name for the customer you can create a mapping for) as well as the testing types assigned to the project, and a random number.
Example input: ACME Corp. + Web App
Example project code: ACME-WEBAPP-192834
PREREQUSITIES:
You must have a SELECT type project custom field which is used to select the customer on the project. This example uses a custom field with a key 'customer'.
You must have a MULTI-SELECT type project custom field which is used to select the testing types assigned to the project. This example uses a custom field with a key 'testing_types'.

Selecting the customer and testing types:

Suggested project code:

Standard library reference
Every built-in AFScript offers is a member of a namespace object in the global scope — Array, String, Object, JSON, XML, Date, Math, Number, Util and Logger — plus the two bare globals Infinity and NaN. All ten namespaces are const: a script can read them, and cannot replace them.
There is no method call syntax in AFScript, so nothing here is invoked on a value. The receiver is the first argument:
Array.isArray, Array.of and String.from are the exceptions — they are statics in JavaScript too, and take no receiver.
Array
Thirty-five members. Every one but Array.isArray and Array.of takes the array as its first argument and throws if it is handed anything else — there is no array-like coercion anywhere in this namespace. The members that mutate their receiver (fill, pop, push, reverse, shift, sort, splice, unshift) mutate it in place and answer what their JavaScript counterparts answer.
Array.entries, Array.keys and Array.values answer arrays, not iterators: AFScript has no iterator protocol, so an iterator would be a value no script could consume. for…of walks an array or a string, so a pair is taken apart by destructuring the header.
Array.at(array, index)
Answers the element at index, counting from the end when index is negative.
array
array
the receiver
index
number
negative counts back from the end
Returns the element, or undefined when index is out of range.
Throws if array is not an array, or index is not a number.
Array.concat(array, ...values)
Answers a new array holding array's elements followed by each of values; an argument that is itself an array is spread one level deep.
array
array
the receiver
...values
any
arrays are flattened one level
Returns a new array. array is unchanged.
Throws if array is not an array.
Array.entries(array)
Answers the index/element pairs.
array
array
the receiver
Returns an array of two-element [index, element] arrays.
Throws if array is not an array.
Differs from JavaScript, which answers an iterator. The array it answers walks the same way, and the pair destructures:
Array.every(array, callback)
Answers whether callback is truthy for every element. Stops at the first falsy answer.
array
array
the receiver
callback
function (element, index, array)
Returns a boolean. An empty array answers true.
Throws if array is not an array, or callback is not a function.
Array.fill(array, value, start?, end?)
Overwrites the elements from start up to but not including end with value, in place.
array
array
the receiver; mutated
value
any
written into every slot in range
start?
number
integer; defaults to 0, negative counts from the end
end?
number
integer; defaults to the array's length
Returns the same array.
Throws if array is not an array, or start/end is present and not an integer.
Array.filter(array, callback)
Answers a new array of the elements for which callback is truthy.
array
array
the receiver
callback
function (element, index, array)
Returns a new array, possibly empty. array is unchanged.
Throws if array is not an array, or callback is not a function.
Array.find(array, callback)
Answers the first element for which callback is truthy.
array
array
the receiver
callback
function (element, index, array)
Returns the element, or undefined if none matches.
Throws if array is not an array, or callback is not a function.
Array.findIndex(array, callback)
Answers the index of the first element for which callback is truthy.
array
array
the receiver
callback
function (element, index, array)
Returns the index, or -1 if none matches.
Throws if array is not an array, or callback is not a function.
Array.findLast(array, callback)
Answers the last element for which callback is truthy, searching from the end.
array
array
the receiver
callback
function (element, index, array)
Returns the element, or undefined if none matches.
Throws if array is not an array, or callback is not a function.
Array.findLastIndex(array, callback)
Answers the index of the last element for which callback is truthy.
array
array
the receiver
callback
function (element, index, array)
Returns the index, or -1 if none matches.
Throws if array is not an array, or callback is not a function.
Array.flat(array, depth?)
Answers a new array with nested arrays spliced into it, down to depth levels.
array
array
the receiver
depth?
number
defaults to 1; pass Infinity to flatten completely
Returns a new array. array is unchanged.
Throws if array is not an array, or depth is present and not a number.
Array.flatMap(array, callback)
Maps every element and flattens the result by one level — Array.map followed by Array.flat(_, 1), in a single pass.
array
array
the receiver
callback
function (element, index, array)
may answer a value or an array
Returns a new array. array is unchanged.
Throws if array is not an array, or callback is not a function.
Array.forEach(array, callback)
Calls callback once for each element, for its effect.
array
array
the receiver
callback
function (element, index, array)
Returns undefined, deliberately — use Array.map when you want the answers.
Throws if array is not an array, or callback is not a function.
Array.from(value, callback?)
Copies an array, or splits a string into its characters.
value
array or string
a string splits by code point
callback?
function (element, index)
maps each element on the way out
Returns a new array.
Throws if value is neither an array nor a string, or callback is present and not a function.
Differs from JavaScript, which answers [] for anything it cannot iterate — Array.from(5) throws here rather than answering an empty array, because a silent empty array is the harder thing to debug. Array-like objects are not accepted. Splitting by code point means an astral character is one element, where String.length would count two UTF-16 units.
Array.includes(array, searchElement, fromIndex?)
Answers whether searchElement is in array, compared with === (except that NaN matches NaN).
array
array
the receiver
searchElement
any
fromIndex?
number
integer; defaults to 0, negative counts from the end
Returns a boolean.
Throws if array is not an array, or fromIndex is present and not an integer.
Array.indexOf(array, searchElement, fromIndex?)
Answers the first index at which searchElement is found, compared with ===.
array
array
the receiver
searchElement
any
fromIndex?
number
integer; defaults to 0, negative counts from the end
Returns the index, or -1.
Throws if array is not an array, or fromIndex is present and not an integer.
Array.isArray(value)
Answers whether value is an array. A static — there is no receiver, and nothing to throw on.
value
any
Returns a boolean.
Array.join(array, separator?)
Concatenates the elements into a string.
array
array
the receiver
separator?
string
defaults to ','
Returns a string. undefined and null elements become the empty string.
Throws if array is not an array, or separator is present and not a string.
Array.keys(array)
Answers the indices.
array
array
the receiver
Returns an array of numbers.
Throws if array is not an array.
Differs from JavaScript, which answers an iterator.
Array.lastIndexOf(array, searchElement, fromIndex?)
Answers the last index at which searchElement is found, searching backwards.
array
array
the receiver
searchElement
any
fromIndex?
number
integer; defaults to the last index
Returns the index, or -1.
Throws if array is not an array, or fromIndex is present and not an integer.
Array.length(array)
Answers the number of elements — AFScript has no property read for it.
array
array
the receiver
Returns a number.
Throws if array is not an array.
Array.map(array, callback)
Answers a new array of callback's answer for each element.
array
array
the receiver
callback
function (element, index, array)
Returns a new array of the same length. array is unchanged.
Throws if array is not an array, or callback is not a function.
Array.of(...values)
Answers an array of its arguments. A static, like its JavaScript original — there is no receiver.
...values
any
every argument becomes one element
Returns a new array.
Differs from JavaScript's new Array(7), not from Array.of(7): this answers a one-element array holding 7, never an array of length seven.
Array.pop(array)
Removes the last element, in place.
array
array
the receiver; mutated
Returns the removed element, or undefined if the array was empty.
Throws if array is not an array.
Array.push(array, ...values)
Appends values, in place.
array
array
the receiver; mutated
...values
any
appended in order
Returns the array's new length.
Throws if array is not an array.
Array.reduce(array, callback, initialValue?)
Folds the array left to right into a single value.
array
array
the receiver
callback
function (accumulator, element, index, array)
initialValue?
any
when omitted, the first element seeds the fold
Returns the final accumulator.
Throws if array is not an array, or callback is not a function. An empty array with no initialValue throws from the host.
Note. An initialValue of undefined is indistinguishable from omitting it, so a fold cannot be seeded with undefined.
Array.reduceRight(array, callback, initialValue?)
Folds the array right to left.
array
array
the receiver
callback
function (accumulator, element, index, array)
initialValue?
any
when omitted, the last element seeds the fold
Returns the final accumulator.
Throws if array is not an array, or callback is not a function.
Array.reverse(array)
Reverses the order of the elements, in place.
array
array
the receiver; mutated
Returns the same array.
Throws if array is not an array.
Array.shift(array)
Removes the first element, in place.
array
array
the receiver; mutated
Returns the removed element, or undefined if the array was empty.
Throws if array is not an array.
Array.slice(array, start?, end?)
Answers a shallow copy of the elements from start up to but not including end.
array
array
the receiver
start?
number
defaults to 0, negative counts from the end
end?
number
defaults to the array's length
Returns a new array. array is unchanged.
Throws if array is not an array.
Note. This member counts its arguments, so Array.slice(xs), Array.slice(xs, undefined) and Array.slice(xs, undefined, undefined) are three different calls — pass real indices rather than undefined placeholders.
Array.some(array, callback)
Answers whether callback is truthy for at least one element. Stops at the first truthy answer.
array
array
the receiver
callback
function (element, index, array)
Returns a boolean. An empty array answers false.
Throws if array is not an array, or callback is not a function.
Array.sort(array, compare?)
Sorts the elements in place.
array
array
the receiver; mutated
compare?
function (a, b)
must declare exactly two parameters
Returns the same array, so a sort can be written inline in a larger expression.
Throws if array is not an array, or compare is given and is not a two-parameter function. Omitting compare sorts by string comparison, as in JavaScript.
Array.splice(array, start, deleteCount?, ...items)
Removes deleteCount elements from start and inserts items there, in place.
array
array
the receiver; mutated
start
number
negative counts from the end
deleteCount?
number
when omitted, everything from start is removed
...items
any
inserted at start
Returns an array of the removed elements.
Throws if array is not an array.
Note. Like Array.slice, this counts its arguments: Array.splice(xs, 1) removes the tail, while Array.splice(xs, 1, undefined) removes nothing.
Array.unshift(array, ...elements)
Inserts elements at the front, in place.
array
array
the receiver; mutated
...elements
any
inserted in the order given
Returns the array's new length.
Throws if array is not an array.
Array.values(array)
Answers a copy of the elements.
array
array
the receiver
Returns a new array.
Throws if array is not an array.
Differs from JavaScript, which answers an iterator.
String
Thirty-two members: twenty-six for text, and six for encoding, hashing and signing. Every one takes the string as its first argument except String.from, which is the namespace's coercion function and takes the value to convert.
Nothing here coerces. String.concat('n: ', 1) throws where JavaScript would quietly answer 'n: 1' — write String.concat('n: ', String.from(1)) when that is what you mean.
The locale-dependent members (localeCompare, toLocaleLowerCase, toLocaleUpperCase) are deliberately absent: their answers vary with the host's ICU build, which makes a script's behaviour depend on where it runs.
Members that take a pattern — match, matchAll, search, split, replace, replaceAll — accept a regular-expression literal (m/…/flags) or a plain string. A string pattern is screened with safe-regex for catastrophic backtracking before it is compiled, and a fresh RegExp is built for every use because lastIndex is observable.
Encoding formats and hash algorithms are listed in the appendices.
String.at(str, index)
Answers the UTF-16 code unit at index, counting from the end when negative.
str
string
the receiver
index
number
must be an integer
Returns a one-character string, or undefined when index is out of range.
Throws if str is not a string, or index is not an integer.
String.charAt(str, index?)
Answers the UTF-16 code unit at index.
str
string
the receiver
index?
number
integer; defaults to 0. Negative values do not count from the end
Returns a one-character string, or '' when index is out of range.
Throws if str is not a string, or index is not an integer.
String.concat(str, ...strings)
Joins strings end to end.
str
string
the receiver
...strings
string
every argument must already be a string
Returns a new string.
Throws if any argument is not a string — unlike JavaScript, which coerces.
String.decode(str, format)
Decodes text that was encoded with String.encode.
str
string
the encoded text
Returns the decoded string, read back as UTF-8.
Throws if either argument is not a string, or format is not a supported format.
String.digest(str, algorithm?, format?)
Hashes a string.
str
string
hashed as UTF-8
Returns the digest, encoded in format. Base16 output is upper-case.
Throws if any argument is not a string, or the algorithm or format is unsupported.
String.encode(str, format)
Encodes a string's UTF-8 bytes.
str
string
the receiver
Returns the encoded string.
Throws if either argument is not a string, format is not supported, or format is 'Z85' and the input is not a whole number of four-byte groups.
String.endsWith(str, searchString, endPosition?)
Answers whether str ends with searchString.
str
string
the receiver
searchString
string
endPosition?
number
integer; treat the string as if it were this long. Defaults to its real length
Returns a boolean.
Throws if str or searchString is not a string, or endPosition is present and not an integer.
String.from(value?)
Converts a value to its string form — the one member that coerces, and the way to opt into coercion everywhere else.
value?
any
omitting it answers ''
Returns a string. Never throws.
Note. This member counts its arguments: String.from() is '', while String.from(undefined) is 'undefined'.
String.hmac(str, key, algorithm?, format?)
Computes a keyed hash (HMAC).
str
string
the message
key
string
the secret
Returns the MAC, encoded in format.
Throws if any argument is not a string, or the algorithm or format is unsupported.
String.includes(str, searchString, position?)
Answers whether searchString occurs in str.
str
string
the receiver
searchString
string
position?
number
integer; where to start looking. Defaults to 0
Returns a boolean.
Throws if str or searchString is not a string, or position is not an integer.
String.indexOf(str, searchString, position?)
Answers the first index at which searchString occurs.
str
string
the receiver
searchString
string
position?
number
integer; where to start looking. Defaults to 0
Returns the index, or -1.
Throws if str or searchString is not a string, or position is not an integer.
String.lastIndexOf(str, searchString, position?)
Answers the last index at which searchString occurs, searching backwards.
str
string
the receiver
searchString
string
position?
number
integer; the last index the match may start at. Defaults to the end of the string
Returns the index, or -1.
Throws if str or searchString is not a string, or position is present and not an integer.
Note. Unlike String.indexOf, an omitted position is not defaulted to 0 — this member searches backwards, so a 0 default would only ever find a match at the very start.
String.length(str)
Answers the number of UTF-16 code units — AFScript has no property read for it.
str
string
the receiver
Returns a number.
Throws if str is not a string.
Note. An astral character counts as two units. Array.length(Array.from('a😀b')) is 3, because Array.from splits by code point.
String.match(str, pattern)
Matches pattern once against str.
str
string
the receiver
pattern
regular expression or string
a string is screened by safe-regex, then compiled with no flags
Returns an array holding the whole match followed by each capture group, or null if the pattern does not match.
Throws if str is not a string, pattern is neither a string nor a regular expression, or a string pattern is unsafe or will not compile.
Differs from JavaScript, which answers a match object carrying index, input and groups. This answers a plain array of strings — those extra properties are not reachable in AFScript anyway.
String.matchAll(str, pattern)
Matches pattern against str as many times as it occurs.
str
string
the receiver
pattern
regular expression or string
the g flag is added when it is missing
Returns an array of match arrays, each holding the whole match followed by its capture groups. An empty array means no match.
Throws on the same conditions as String.match.
Differs from JavaScript, which refuses a non-global pattern outright. AFScript adds the flag to the pattern the call will use instead — the literal handed in is never modified.
String.padEnd(str, targetLength, padString?)
Pads the end of str until it is targetLength long.
str
string
the receiver
targetLength
number
must be an integer
padString?
string
repeated as needed and truncated to fit; defaults to ' '
Returns a new string; the original when it is already long enough.
Throws if str or padString is not a string, or targetLength is not an integer.
String.padStart(str, targetLength, padString?)
Pads the front of str until it is targetLength long.
str
string
the receiver
targetLength
number
must be an integer
padString?
string
repeated as needed and truncated to fit; defaults to ' '
Returns a new string.
Throws if str or padString is not a string, or targetLength is not an integer.
String.repeat(str, count)
Repeats str.
str
string
the receiver
count
number
integer, zero or greater
Returns a new string; '' when count is 0.
Throws if str is not a string, or count is negative or not an integer. The check is made here rather than in the host, so the message names String.repeat.
String.replace(str, pattern, replacement)
Replaces the first match of pattern — or every match, if pattern carries the g flag.
str
string
the receiver
pattern
string or regular expression
a plain string matches literally, once
replacement
string or function (match, ...groups, offset, str)
$&, $1… are honoured in a string replacement
Returns a new string.
Throws if str is not a string, pattern is falsy, or replacement is neither a string nor a function.
String.replaceAll(str, pattern, replacement)
Replaces every match of pattern.
str
string
the receiver
pattern
string or regular expression
a regular expression must carry the g flag
replacement
string or function (match, ...groups, offset, str)
Returns a new string.
Throws on the same conditions as String.replace.
String.search(str, pattern)
Finds where pattern first matches.
str
string
the receiver
pattern
regular expression or string
screened by safe-regex when it is a string
Returns the index of the first match, or -1.
Throws if str is not a string, pattern is neither a string nor a regular expression, or a string pattern is unsafe or will not compile.
String.sign(str, privateKeyPem, algorithm?, encoding?)
Signs a string with a PEM-encoded private key.
str
string
the payload
privateKeyPem
string
PEM private key
Returns the signature, encoded in encoding.
Throws if any argument is not a string, the algorithm or encoding is unsupported, or the host rejects the key.
Note. The argument order is not the mirror of String.verify: signing takes (algorithm, encoding), verifying takes (encoding, algorithm).
String.slice(str, indexStart, indexEnd?)
Answers the substring from indexStart up to but not including indexEnd.
str
string
the receiver
indexStart
number
integer, required; negative counts from the end
indexEnd?
number
integer; defaults to the end of the string
Returns a new string; '' when the range is empty.
Throws if str is not a string, or either index is present and not an integer.
String.split(str, separator, limit?)
Splits str into an array.
str
string
the receiver
separator
string or regular expression
'' splits into UTF-16 code units
limit?
number
integer, zero or greater; stop after this many pieces
Returns an array of strings.
Throws if str is not a string, separator is neither a string nor a regular expression, or limit is present and is not a non-negative integer.
String.startsWith(str, searchString, position?)
Answers whether str starts with searchString.
str
string
the receiver
searchString
string
position?
number
integer; start the comparison here. Defaults to 0
Returns a boolean.
Throws if str or searchString is not a string, or position is not an integer.
String.substring(str, indexStart, indexEnd?)
Answers the substring between two indices, swapping them if they are the wrong way round.
str
string
the receiver
indexStart
number
integer, required
indexEnd?
number
integer; defaults to the end of the string
Returns a new string.
Throws if str is not a string, or either index is present and not an integer.
Note. Negative indices clamp to 0 here, where String.slice counts them from the end.
String.toLowerCase(str)
Answers str in lower case.
str
string
the receiver
Returns a new string.
Throws if str is not a string.
String.toUpperCase(str)
Answers str in upper case.
str
string
the receiver
Returns a new string.
Throws if str is not a string.
String.trim(str)
Removes whitespace from both ends.
str
string
the receiver
Returns a new string.
Throws if str is not a string.
String.trimEnd(str)
Removes trailing whitespace.
str
string
the receiver
Returns a new string.
Throws if str is not a string.
String.trimStart(str)
Removes leading whitespace.
str
string
the receiver
Returns a new string.
Throws if str is not a string.
String.verify(str, publicKeyPem, signature, encoding?, algorithm?)
Checks a signature made by String.sign.
str
string
the payload that was signed
publicKeyPem
string
PEM public key
signature
string
as produced by String.sign
encoding?
string
how signature is encoded; defaults to 'base64'
Returns true when the signature is valid, false when it is not.
Throws if any argument is not a string, or the encoding or algorithm is unsupported.
Note. encoding comes before algorithm here and after it in String.sign. Passing String.sign's argument order to String.verify throws rather than answering false.
Object
Three members. All three accept any non-null object — including an array, whose keys are its indices as strings — and throw on anything else, so Object.keys('abc') is an error rather than ['0', '1', '2'].
Own properties only: AFScript refuses every inherited member read, so there is nothing else to see. There is no assign, freeze, fromEntries or hasOwn.
Object.entries(obj)
Answers the object's own key/value pairs.
obj
object or array
Returns an array of two-element [key, value] arrays, in insertion order.
Throws if obj is not an object, or is null.
Object.keys(obj)
Answers the object's own keys.
obj
object or array
Returns an array of strings.
Throws if obj is not an object, or is null.
Object.values(obj)
Answers the object's own values.
obj
object or array
Returns an array of values, in the same order as Object.keys.
Throws if obj is not an object, or is null.
JSON
JSON.parse(text)
Parses JSON text.
text
string
Returns the parsed value, or undefined if text is not valid JSON.
Never throws — a script that cares about the difference should test the answer.
Differs from JavaScript, which throws a SyntaxError. There is no reviver argument.
JSON.stringify(value, space?)
Renders a value as JSON text.
value
any
space?
number or string
indentation; anything else is ignored
Returns a JSON string, or undefined for a value JSON cannot represent (undefined, or a function).
Differs from JavaScript: there is no replacer argument, so space is the second argument rather than the third.
XML
XML.parse(xml)
Parses an XML document into plain arrays and objects.
xml
string
Returns an array of nodes in document order, or undefined if the document is malformed. Attributes are gathered under ':@' with an @_ prefix on each name; text is a #text node.
Throws only if xml is not a string.
Hardened: DOCTYPE declarations are stripped and entity processing is off, so XXE and billion-laughs inputs are inert. Processing instructions and the XML declaration are dropped, and the result is copied before it reaches the script. Parser errors are swallowed rather than reported — answering undefined tells a script everything it can act on, and a stack trace would disclose the host's paths. There is no XML.stringify.
Date
Two members. Both accept the same timeValue: the string 'now', an ISO-ish string, or an integer epoch in milliseconds. The accepted string forms are
YYYY-MM-DDYYYY-MM-DD HH:MMorYYYY-MM-DDTHH:MM, optionally with a trailingZYYYY-MM-DD HH:MM:SSorYYYY-MM-DDTHH:MM:SS, optionally with a trailingZYYYY-MM-DD HH:MM:SS.mmmorYYYY-MM-DDTHH:MM:SS.mmm, optionally with a trailingZ
and every one of them is read as UTC. Anything else throws. There is no Date value type in AFScript: a timestamp is a string or a number, and these two members are how you move between them.
Date.datetime(timeValue?, ...modifiers)
Normalises a timestamp and applies modifiers to it, left to right, in UTC.
timeValue?
string or number
'now', an accepted date string, or an integer epoch. Omitted means now
Returns an ISO-8601 string — or a number, if the last modifier reached was 'epoch'.
Throws if timeValue is neither an accepted string nor an integer. An unrecognised modifier is silently ignored.
Note. Month and year arithmetic clamps to the last valid day of the target month rather than overflowing into the next one: 31 January plus one month is 29 February in a leap year, not 2 March.
Date.format(timeValue, mask, tz?)
Renders a timestamp through a dateformat-style mask.
timeValue
string or number
as for Date.datetime
mask
string
a token mask, or the name of a built-in mask
tz?
string
a timezone name, e.g. 'UTC'. Defaults to the host's local timezone
Returns a string.
Throws if mask is not a string, tz is present and not a string, or timeValue is not an accepted form.
Note. With no tz, output is rendered in whatever timezone the host runs in — the same script can answer differently on two machines. Pass 'UTC', or prefix the mask with UTC:, when the answer must be stable.
Mask tokens, shown for 2020-06-09T13:05:07.089Z in UTC:
d dd
9 09
h hh
1 01 (12-hour)
ddd dddd
Tue Tuesday
H HH
13 13 (24-hour)
m mm
6 06
M MM
5 05 (minutes)
mmm mmmm
Jun June
s ss
7 07
yy yyyy
20 2020
l L
089 08 (milliseconds)
S
th (ordinal suffix)
t tt T TT
p pm P PM
N
2 (ISO weekday)
Z o
UTC +0000
W
24 (ISO week)
Built-in mask names: default, shortDate, paddedShortDate, mediumDate, longDate, fullDate, shortTime, mediumTime, longTime, isoDate, isoTime, isoDateTime, isoUtcDateTime, expiresHeaderFormat.
Math
Eight constants and thirty-seven functions. All but Math.secureRandom and Math.secureRandomInt are the host's own Math intrinsics, passed through unwrapped — they behave exactly as they do in JavaScript, including its coercion rules, and answer NaN for arguments they cannot use rather than throwing. Their entries are written in short form.
Math.random() is the platform PRNG: fast, seeded per realm, and not a security primitive. When unpredictability matters, use Math.secureRandom() or Math.secureRandomInt().
Math.E
Euler's number, the base of the natural logarithm.
Math.LN10
The natural logarithm of 10.
Math.LN2
The natural logarithm of 2.
Math.LOG10E
The base-10 logarithm of E.
Math.LOG2E
The base-2 logarithm of E.
Math.PI
The ratio of a circle's circumference to its diameter.
Math.SQRT1_2
The square root of ½.
Math.SQRT2
The square root of 2.
Math.abs(x)
Answers the absolute value of x.
Math.acos(x)
Answers the arc cosine of x, in radians. NaN outside [-1, 1].
Math.acosh(x)
Answers the hyperbolic arc cosine of x. NaN below 1.
Math.asin(x)
Answers the arc sine of x, in radians. NaN outside [-1, 1].
Math.asinh(x)
Answers the hyperbolic arc sine of x.
Math.atan(x)
Answers the arc tangent of x, in radians, between -π/2 and π/2.
Math.atan2(y, x)
Answers the angle in radians between the positive x-axis and the point (x, y). Note that y comes first.
Math.atanh(x)
Answers the hyperbolic arc tangent of x. NaN outside (-1, 1).
Math.cbrt(x)
Answers the cube root of x.
Math.ceil(x)
Answers the smallest integer greater than or equal to x.
Math.clz32(x)
Answers the number of leading zero bits in the 32-bit binary form of x.
Math.cos(x)
Answers the cosine of x, which is in radians.
Math.cosh(x)
Answers the hyperbolic cosine of x.
Math.exp(x)
Answers E raised to the power x.
Math.expm1(x)
Answers Math.exp(x) - 1, accurately for small x.
Math.floor(x)
Answers the largest integer less than or equal to x.
Math.fround(x)
Answers the nearest 32-bit single-precision float to x.
Math.hypot(...values)
Answers the square root of the sum of the squares of its arguments.
Math.imul(a, b)
Answers the result of 32-bit integer multiplication, with C-like wrapping.
Math.log(x)
Answers the natural logarithm of x. NaN for negative x, -Infinity for 0.
Math.log10(x)
Answers the base-10 logarithm of x.
Math.log1p(x)
Answers Math.log(1 + x), accurately for small x.
Math.log2(x)
Answers the base-2 logarithm of x.
Math.max(...values)
Answers the largest of its arguments.
Note. The largest element of an array is found by spreading it:
Math.min(...values)
Answers the smallest of its arguments.
Math.pow(base, exponent)
Answers base raised to the power exponent — the same as the ** operator.
Math.random()
Answers a pseudo-random float in [0, 1) from the platform PRNG.
Not a security primitive — see Math.secureRandom.
Math.round(x)
Answers x rounded to the nearest integer; a half rounds towards +Infinity.
Math.secureRandom()
Answers a float in [0, 1) drawn from the platform CSPRNG.
Returns a number in the same interval Math.random() uses — never 1 — so it drops straight into the usual index idiom. Fifty-three bits of randomness, the most a double can carry.
Math.secureRandomInt(min, max)
Answers an integer in [min, max], inclusive at both ends, drawn from the platform CSPRNG by rejection sampling so that no value is favoured.
min
number
a safe integer; may be negative
max
number
a safe integer, not less than min
Returns a number.
Throws if either argument is not a safe integer, if min > max, or if max - min + 1 is not itself a safe integer.
Math.sign(x)
Answers 1, 0 or -1 according to the sign of x.
Math.sin(x)
Answers the sine of x, which is in radians.
Math.sinh(x)
Answers the hyperbolic sine of x.
Math.sqrt(x)
Answers the square root of x. NaN for negative x.
Math.tan(x)
Answers the tangent of x, which is in radians.
Math.tanh(x
Answers the hyperbolic tangent of x.
Math.trunc(x)
Answers the integer part of x, dropping any fractional digits.
Number
Eight constants and six functions, all of them the host's Number intrinsics passed through unwrapped. There is no Number(...) conversion function — use Number.parseInt or Number.parseFloat — and no prototype members, so there is no toFixed.
Number.NaN and the global NaN are the same value; so are Number.POSITIVE_INFINITY and the global Infinity.
Number.EPSILON
The difference between 1 and the smallest float greater than 1 — the usual tolerance for comparing two floats.
Number.MAX_SAFE_INTEGER
The largest integer that can be represented exactly, 2⁵³ − 1.
Number.MAX_VALUE
The largest representable positive number.
Number.MIN_SAFE_INTEGER
The smallest integer that can be represented exactly, −(2⁵³ − 1).
Number.MIN_VALUE
The smallest representable positive number.
Number.NaN
Not-a-Number. Equal to nothing, including itself — test for it with Number.isNaN.
Number.NEGATIVE_INFINITY
Negative infinity.
Number.POSITIVE_INFINITY
Positive infinity.
Number.isFinite(value)
Answers whether value is a number and is neither infinite nor NaN. Does not coerce, so a numeric string answers false.
Number.isInteger(value)
Answers whether value is a number with no fractional part.
Number.isNaN(value)
Answers whether value is exactly NaN. Does not coerce, so a non-numeric string answers false.
Number.isSafeInteger(value)
Answers whether value is an integer that can be represented exactly.
Number.parseFloat(string)
Reads a decimal number from the front of a string.
Returns the number, or NaN if the string does not start with one. Trailing text is ignored.
Number.parseInt(string, radix?)
Reads an integer from the front of a string.
string
string
leading whitespace is skipped
radix?
number
2 to 36; always pass it when the input may be user-supplied
Returns the integer, or NaN.
Util
Two members, both backed by the platform CSPRNG.
Util.randomId(format, nbytes?)
Answers a random identifier.
nbytes?
number
how many random bytes to draw; a positive integer, defaults to 32
Returns the encoded bytes as a string. The string is longer than nbytes — base64 answers about 4 characters per 3 bytes, base16 exactly 2 per byte.
Throws if format is unsupported, nbytes is not a positive integer, or format is 'Z85' and nbytes is not divisible by 4.
Util.uuidv4()
Answers a random (version 4) UUID.
Returns a 36-character lower-case string in the canonical 8-4-4-4-12 form.
Logger
Six members, one per level, from most to least severe: fatal, error, warn, info, debug, trace. They are a script's only output — there is no console, and the interpreter never writes to the host's own.
Each takes any number of values, converts each to a string (undefined and null become 'undefined' and 'null'), joins them with a single space, and emits the result at its level. A message below the configured logLevel is dropped. All six answer undefined.
Where the message goes is the host's business: the AFScript class emits a message event carrying { logLevel, message }.
Logger.fatal(...values)
Emits at level FATAL (1) — the level that is never filtered out.
Logger.error(...values)
Emits at level ERROR (2).
Logger.warn(...values)
Emits at level WARN (3).
Logger.info(...values)
Emits at level INFO (4).
Logger.debug(...values)
Emits at level DEBUG (5). Objects and arrays are worth wrapping in JSON.stringify — plain conversion answers [object Object], as it does in JavaScript.
Logger.trace(...values)
Emits at level TRACE (6), the noisiest level.
Global values
Infinity
Positive infinity — the same value as Number.POSITIVE_INFINITY.
NaN
Not-a-Number — the same value as Number.NaN. It compares equal to nothing, itself included.
These two are the only global values the standard library defines. Everything else a script sees in the global scope is a namespace object, or a symbol the host passed in.
Appendix: encoding formats
The same six formats are accepted by String.encode, String.decode, String.digest, String.hmac, String.sign, String.verify and Util.randomId. They are matched exactly — note the capitals on Z85.
Format
String.encode('abcd', …)
Notes
'base64'
YWJjZA==
RFC 4648 §4, padded with =
'base64url'
YWJjZA==
RFC 4648 §5 — - and _ in place of + and /, so the result is URL- and filename-safe
'base32'
MFRGGZA=
RFC 4648 §6, upper-case
'base32hex'
C5H66P0=
RFC 4648 §7 — the extended-hex alphabet, which sorts in the same order as the bytes it encodes
'base16'
61626364
Hexadecimal, upper-case
'Z85'
vpA.S
ZeroMQ Z85 — the most compact of the six, at 5 characters per 4 bytes
Z85 only accepts whole four-byte groups. String.encode('hello', 'Z85') throws, because five bytes is not a multiple of four; so does Util.randomId('Z85', 6). Decoding it needs a whole number of five-character groups. Hash digests are always a multiple of four bytes, so String.digest and String.hmac can use 'Z85' with any algorithm.
Appendix: hash algorithms
Accepted by String.digest, String.hmac, String.sign and String.verify, matched exactly:
'MD5'
128 bits
Broken. Checksums only, never signatures or passwords
'RMD160'
160 bits
RIPEMD-160
'SHA1'
160 bits
Collision-broken. Legacy interoperability only
'SHA224'
224 bits
'SHA256'
256 bits
The default everywhere, and the right choice unless something external dictates otherwise
'SHA384'
384 bits
'SHA512'
512 bits
Defaults: String.digest and String.hmac answer SHA256 in base16; String.sign and String.verify use SHA256 and base64.
Neither hashing nor signing is a password primitive — there is no key-derivation function here, and String.digest is not one.
Appendix: Date.datetime modifiers
Date.datetime applies its modifiers left to right, in UTC, to an immutable value. A modifier it does not recognise is silently ignored.
'+N days' / '-N days'
Adds or subtracts whole days. N is 1 to 6 digits
'+N hours' / '-N hours'
Hours
'+N minutes' / '-N minutes'
Minutes
'+N seconds' / '-N seconds'
Seconds. May carry milliseconds as '+1.500 seconds' — exactly three digits
'+N months' / '-N months'
Months, clamping to the last valid day of the target month
'+N years' / '-N years'
Years, clamping the same way
'start of day'
Truncates to 00:00:00.000
'start of month'
Truncates to the first of the month
'start of year'
Truncates to 1 January
'isostring'
Terminal. Answers an ISO-8601 string immediately. This is the default anyway
'epoch'
Terminal. Answers the epoch in milliseconds, as a number, immediately
The sign is required on the arithmetic modifiers — '1 days' is not recognised, and being unrecognised, does nothing. A terminal modifier ends the chain, so anything after 'epoch' is never applied.
Last updated