Introducing Streamscript

Streamscript connects APIs with data using formula-like statements and actions. It runs inside Flow so it benefits from data security settings, flow versioning and flow tests. Streamscript enables flows to perform HTTP callouts and respond to webhooks, for example:

 

Getting started

  1. Install the Streams package in Sandbox or Production (direct URL)
  2. Go to Setup > Flows > New Autolaunched Flow
  3. Add an action element, type: Streamscript
  4. Copy and paste the below exchange rate example into your step:
# Streamscript
$http = GET 'api.exchangerate.host/EUR'
$result = JSON-Decode $http.body
return $result.rates.USDTry

Save the flow, then click debug. That's it! You just integrated live exchange rates into your flow. You'll need to add a Remote Site Setting for the example. Then use the editor in flow builder to test and iterate your logic.

Alternatively, install from the Salesforce AppExchange (listing URL)

 

Video - HTTP Callout

Shows a flow calling an API in 90 seconds. Uses template strings, URL variables, the dot . to traverse JSON collections, and math commands. Returns data available as a flow resource.

Video - Business Hours

Shows how to link Business Hours (or other native logic) directly into flows. Uses concepts already found in formula fields such as the {!$Flow.InterviewStartTime} global.

Video - Webhooks in Flow

Example using webhooks to link GitHub issues with Salesforce cases. Shows the integrations tab, the webhook endpoint, catching the payload, and returning SObject data.

How to read the JSON payload to store GitHub issue details on cases, or log case comments:

# Streamscript
$request = Json-Decode $Webhook.request

# Case record
if ($request.issue)
{
    $Case = New-Case
    $Case.Subject = $request.issue.html_url
    $Case.Description = $request.issue.title
    $Case.Description += "\n" + $request.issue.body
    $Case.Description += "\n" + $request.issue.user.login
    $Case.Description += "\n" + $request.issue.created_at.format()
    $Case.Description += "\n" + $request.issue.html_url
}

# Case Comment records
if ($request.comment)
{
    $CaseComment = New-CaseComment
    $CaseComment.CommentBody = $request.comment.body
    $CaseComment.CommentBody += "\n" + $request.comment.user.login
    $CaseComment.CommentBody += "\n" + $request.comment.created_at.format()
    $CaseComment.CommentBody += "\n" + $request.comment.html_url
    $CaseComment = [$CaseComment]
}

return -record $Case -records $CaseCommentTry

Run flow in System Mode so the Site Guest User works. It can query existing records like this:

Going further you may secure the GitHub webhook by verifying signatures, for example:

# secure compare via custom metadata
$secret = Base64-Encode {!secret_mdt}
$payload = Base64-Encode $Webhook.request
$mac = Hex-Decode $Webhook.requestHeaders.X-Hub-Signature-256.substr(7)
if (!Crypto-Verify-Hmac 'hmacSHA256' $payload $secret $mac) {throw $mac}Try

Blog Posts

Streams Q&A

Q: Where is the package?
Here is the install URL: /packaging/installPackage.apexp?p0=04t7F000005N1wy

Q: I'm not a coder, can I use Streamscript?
Yes. If you've used formulas, it will feel familiar. This ChatGPT video may help you get started.

Q: Doesn't this overlap Salesforce functionality?
Yes. Platform aspects like HTTP callouts have changed since the first release of Streams. We recommend using declarative features where possible. Check out these alternatives, each with a unique approach to bring a specific area of focus into Flow:
- Apex actions - Chris, Professor Flow
- Slack integration - Jack Pond, UnofficialSF
- Callouts (Beta) Spring '23 - Joseph Thomas, P&N Bank
- Mini Postman with Invocable Callout - Munawir Rahman at Gojek

Q: I don't see the point. Can you explain where script is used?
Script solves immediate needs (like webhooks) without waiting for declarative tools. Flow is for business processes and users may choose the approach best suited to their business. The right tool for the job may be config or code or a combination of both.

Q: Functions wrap logic in general form. Can I call functions in other scripts?
No. Scripts are self-contained and don't reference external dependencies. This ensures a script can be versioned within flow and deployed to any environment as a single logical unit.

Q: How can I send auth headers in my HTTP callout?
Set up a Named Credential then use as follows: Http-Post 'callout:My_Cred/path/to/api'
Avoid handling session ids, usernames, passwords or authentication tokens within your logic.

Q: Is the package security reviewed?
Yes. Latest details here. Streams is published on the AppExchange with Aloha status from Salesforce. At install you can click 'View Components' to verify it contains no integrations.

Q: Is this related to OmniScript, Mule scripts, or AMPscript?
No. Those are external hosted environments. Streamscript only runs in native Salesforce flows.

Q: How do I access the results of script in subsequent steps?
One-line scripts automatically return the result of the expression. Streamscript will set the output variable (num, bool, text, list, record, records) according to the result data type.
Multi-line scripts use the return keyword. For example: return $num or return $list
Note: when returning SObjects, the variable name must be the type, eg: return $Account

Q: Does it support bulkification?
Yes. Best practice for record-triggered flows is to use the Bulk directive. In bulk mode, Streamscript will run once instead of many times. Inputs or merge fields will contain lists instead of a single value. Outputs or return values will expect lists instead of a single value.

Q: Is this free? What do you gain in return?
Yes. In return we hope you'll consider us for your next project.

Q: Can I offer script capabilities in my company's package?
Yes. Contact us and we will assist.

Q: How do I get syntax highlighting and autocompletions?
We use this optional Chrome Extension in our videos.

Q: How do I suggest an idea for Streamscript?
Thanks for thinking of us! Please read our policy first. It protects both you and us from any potential legal issues that may arise if we were already working on a similar idea.

Q: Can I post on your blog?
Sorry, we're not currently looking for Streamscript bloggers. We celebrate success stories in our occasional newsletter, so please don't contact us to "blog about X" in exchange for Y.

Streamscript Tutorial and Reference

Contents Commands Methods

Here are the basics.

Comments begin with a hash # sign.

# Streamscript

Variables begin with a dollar $ sign and are case sensitive. Use an equals = sign to set values.

$account = 'GenePoint'Try

Data types are text, number, boolean, list, and map, like JSON.

$text = 'Hi!'
$number = 12345
$boolean = false
$list = ['GenePoint', 'GenWatt']
$map = {LastName: 'Smith', AccountId: '001000000000000AAA'}Try

Templates are text in ` backticks. You can embed $variables and ${expressions} in templates.

$author = 'Mark Twain'
$quote = 'Reports of my death are greatly exaggerated'
Log `$author - "$quote"` # Mark Twain - "Reports of my death are greatly exaggerated"Try

Operators are used to compare variables and make formulas.

$profit = $sales - $costs
$success = $profit > 10000
$vacation = $success and $weather == 'sunny'Try

Methods operate on any variable using the call .() operator. Parentheses are optional.

$greeting = 'Hello World'
$length = $greeting.length             # 11
$shortGreeting = $greeting.left(5)     # Hello
$loudGreeting = 'Hello World'.upper    # HELLO WORLDTry

Commands are actions. Each command runs by itself, unlike methods which act on a variable.

Log 'I had no idea Abraham Lincoln loved cats.' # Logs appear in the sidebarTry

Decisions use the switch and if keywords. For alternative branches use elseif and else.

if ($weather == 'sunny')
{
    # run if weather is sunny
}

switch ($weather)
{
    hot { ... } # run when weather is hot
    cold { ... } # run when weather is cold
    default { ... } # run when weather is anything else
}

# if-else in one line (ternary operator)
$ticket = $age > 18 ? 'Adult' : 'Child'
$dealtype = $staff < 100 ? 'SMB' : 'Enterprise'Try

Loops repeat the logic between the { curly } braces. Streamscript supports while, foreach, for and do-while loops. Use continue to skip an iteration, or break to terminate the loop.

$condition = false
while ($condition)
{
    # runs while condition is true
}

$items = [1, 2, 3, 4, 5]
foreach ($item in $items)
{
    # loops over all the items in the list
}Try

Merge Fields also known as formula tags are used to import flow resources including flow constants, flow globals, flow variables, records, and collections.  

$var = {!var}
$const = {!constant}
$formula = {!formula}
$Opportunity = {!$Record}Try

A complete list of commands and methods follows below. Some methods may seem familiar given their similarity to Salesforce formula functions. Examples follow straight after.

HTTP commands

# Send a GET or POST request 
$Http = Http-Get $url $headers
$Http = Http-Post $url $headers $body
$Http = Http-Post $url $headers $body -base64
$Http = Http-Patch $url $headers $body
$Http = Http-Put $url $headers $body
$Http = Http-Delete $url $headers
# optional base64 flag treats the request and response body as encoded binary data

# Response map
$Http.body       # Represents the text of the body returned in the response
$Http.status     # Represents the integer status code returned in the response
$Http.headers    # Represents the map of HTTP headers returned in the responseTry

Date/Time commands

# Return the text value representing a datetime, eg '2022-12-24T23:59:59.999Z'
$dt = Datetime-Value $year $month $day $hour $minute $second $millis -gmt

# Return the text value representing a date, eg '2022-12-24'
$date = Date-Value $year $month $day

# Return the text value representing a time, eg '23:59:59.999Z'
$time = Time-Value $hour $minute $second $millis

# Return the time zone offset (in milliseconds) between the datetime and GMT
$millis = Timezone-Offset $timezoneId $datetime

Business Hours commands

# Add a time interval to the start datetime, adjusting for business hours
$dt = BusinessHours-Add $businessHoursId $startDatetime $intervalMillis -gmt

# Return the number of milliseconds between two datetimes, adjusting for business hours
$int = BusinessHours-Diff $businessHoursId $startDatetime $endDatetime

# Return true if the target datetime occurs within business hours, accounting for holidays
$bool = BusinessHours-IsWithin $businessHoursId $targetDatetime

# For the target datetime, return next datetime when business hours are open, accounting for holidays
$dt = BusinessHours-NextStartDate $businessHoursId $targetDatetime

Encode/Decode commands

# Convert the variable to JSON text and vice versa
$text = Json-Encode $var
 $var = Json-Decode $text

# Convert unsafe characters to URL-safe percent encoded format and vice versa
$code = Url-Encode $text
$text = Url-Decode $code

# Convert the Base64 encoded binary value to text and vice versa
$text = Base64-Decode $b64
 $b64 = Base64-Encode $text

Record commands

# Prepare a record using a map or parameters
$Account = New-Account {Name: 'ACME'}
$Contact = New-Contact -LastName 'Smith'

Info commands

# Current user information
$id = User-Info -id                    # Return the user's ID
$name = User-Info -name                # Return the user's full name
$email = User-Info -email              # Return the user's email address
$locale = User-Info -locale            # Return the user's locale, eg 'en_CA'
$userName = User-Info -userName        # Return the user's login name
$language = User-Info -language        # Return the user's language, eg 'en_US' or 'de'
$userType = User-Info -userType        # Return the user's type, eg 'Standard'
$timeZone = User-Info -timeZone        # Return the user's time zone, eg 'Asia/Tokyo'
$profileId = User-Info -profileId      # Return the user's profile ID
$userRoleId = User-Info -userRoleId    # Return the user's role IDTry
# Organization information
$id = Org-Info -id          # Return the organization ID
$name = Org-Info -name      # Return the organization name
$url = Org-Info -url        # Return the unique org URL, eg 'https://org.my.salesforce.com'Try
# URL information
$text = Url-Info $url -host        # Return the host part of the URL
$text = Url-Info $url -path        # Return the path part of the URL
$text = Url-Info $url -port        # Return the port part of the URL
$text = Url-Info $url -protocol    # Return the protocol part of the URL
 $map = Url-Info $url -query       # Return the URL's query parameters as a mapTry

List commands

# Commands that transform the list:
$list = List-Apply   $list ${...}    # Apply the logic in ${...} to each item in the list
$list = List-Sort    $list ${...}    # Sort the list using the sort logic in ${...}
$list = List-Reverse $list           # Reverse the order of the list items
# Commands that return a new list:
$new = List-Copy     $list           # Return a deep copy of the list 
$new = List-Filter   $list ${...}    # Return items that meet the condition in ${...}
$new = List-Unique   $list           # Return the list after removing duplicate items
$new = List-First    $list $N        # Return the first (or first N) items of the list
$new = List-Last     $list $N        # Return the last (or last N) items of the list
$new = List-Union    $list $list2    # Return $list and $list2 combined into a single list
$new = List-Project  $list ${...}    # Return a new list transformed by the logic in ${...}
$map = List-ToMap    $list ${...}    # Return a map, keyed on the logic in ${...}
# Commands that aggregate a result:
$num = List-Sum $list ${...}      # Sum across each number retrieved by logic in ${...}
$num = List-Avg $list ${...}      # Average across each number retrieved by logic in ${...}
$num = List-Min $list ${...}      # Return the lowest number retrieved by logic in ${...}
$num = List-Max $list ${...}      # Return the highest number retrieved by logic in ${...}
$txt = List-Concat $list $delim   # Concatenate the list items into a single text value

Crypto commands

$b64 = Crypto-Encode $algo $key $cleartext $iv    # Encrypt cleartext with optional IV
$b64 = Crypto-Decode $algo $key $ciphertext $iv   # Decrypt ciphertext with optional IV
$b64 = Crypto-Generate-Key $size                  # Generates AES key of size
$b64 = Crypto-Generate-Digest $algo $data         # Compute one-way hash digest
$b64 = Crypto-Generate-Mac $algo $data $key       # Compute message auth code (MAC)
$b64 = Crypto-Sign $algo $input $keyOrCert        # Compute digital signature
$bool= Crypto-Verify $algo $data $sig $keyOrCert  # Verify a digital signature
$bool= Crypto-Verify-Hmac $algo $input $key $mac  # Verify with secure compare

Script commands

$bool = Script-IsTest           # Return true when running under test mode.
 $int = Script-Epoch            # Return current time as milliseconds since 1970-01-01 GMT
 $num = Script-Random           # Return a positive number greater than 0.0 and less than 1.0
  $dt = Script-Now              # Return current datetime, eg '2022-12-24T23:59:59.999Z'
$date = Script-Today            # Return today's date referencing the current user time zone
  $id = Script-Defer ${...}     # Enqueue logic and return ID of the queueable job
        Script-Log $message     # Write the specified message to the script log

Text methods

$bool = $text.isBlank()           # Return true if the text is empty '' or null
 $int = $text.length()            # Return how many Unicode characters the text contains
$bool = $text.startsWith($prefix) # Return true if the text begins with the specified prefix
$bool = $text.contains($fragment) # Return true if the text contains the fragment
$bool = $text.endsWith($suffix)   # Return true if the text ends with the specified suffix
$list = $text.chars()             # Return the list of character codes representing the text
 $int = $text.charAt($index)      # Return the value of the character at the specified index
 $int = $text.find($fragment)     # Return the index of the first occurrence, or -1 otherwise
$text = $text.left($length)       # Return the leftmost characters for the specified length
$text = $text.mid($start, $len)   # Return the text from the start index to the length given
$text = $text.right($length)      # Return the rightmost characters for the specified length
$text = $text.replace($find, $re) # Replace each $find occurrence in $text with $replace
$list = $text.split($regexp)      # Return a list of text parts, split by regular expression*
$text = $text.substr($beg, $end)  # Return the part of text between the start and end index
$text = $text.lower()             # Return the text with all letters converted to lowercase
$text = $text.upper()             # Return the text with all letters converted to uppercase
$text = $text.trim()              # Return the text with no leading nor trailing white space
$text = $index.choice($labels)    # Return Nth label or argument, eg 'None', 'One', 'Many'

Number methods

$num = $num.abs()             # Return the absolute value of the number
$num = $num.pow($exp)         # Return the value of the number to the power of exponent
$num = $num.round()           # Return the rounded number using half-even rounding mode
$num = $num.mod($divisor)     # Return the remainder of the number divided by the divisor
$odd = $num.isOdd()           # Return true for odd numbers 1, 3, 5...

Boolean operators

$bool = $a == $b                # Equals
$bool = $a != $b                # Not equal
$bool = $a <  $b                # Less Than
$bool = $a <= $b                # Less than or equal to
$bool = $a >= $b                # Greater than or equal to
$bool = $a >  $b                # Greater than
$bool = $a in $list             # Is value in a list
$bool = $a notin $list          # Is value missing from a list
$bool = $a like $wildcard       # Like wildcard pattern*
$bool = $a notlike $wildcard    # Not like wildcard pattern
$bool = $a match $regexp        # Matches regular expression*
$bool = $a notmatch $regexp     # Not matches regular expression
$bool = $a and $b               # && Boolean AND
$bool = $a or $b                # || Boolean OR
$bool = not $a                  # ! Boolean NOT

Date methods

 $int = $date.year()             # Return the year part of the date
 $int = $date.month()            # Return the month part of the date, eg January = 1
 $int = $date.day()              # Return the day-of-month part of the date
$date = $date.addYears($years)   # Add the given number of years to the returned date
$date = $date.addMonths($months) # Add the given number of months to the returned date
$date = $date.addDays($days)     # Add the given number of days to the returned date

Datetime methods

 $int = $dt.epoch()              # Return datetime as milliseconds since 1970-01-01 GMT
$date = $dt.date($gmt)           # Return date in the user time zone (or GMT if arg is true)
$time = $dt.time($gmt)           # Return time in the user time zone (or GMT if arg is true)
 $int = $dt.dayOfWeek($gmt)      # Return the weekday, eg Monday = 1, Tuesday = 2, etc
$text = $dt.format($format, $tz) # Format datetime using a specific format* and time zone

Time methods

 $int = $time.hour()               # Return the hour part of the time
 $int = $time.minute()             # Return the minute part of the time
 $int = $time.second()             # Return the second part of the time
 $int = $time.millis()             # Return the millisecond part of the time
$time = $time.addHours($hours)     # Add the number of hours to the returned time
$time = $time.addMinutes($minutes) # Add the number of minutes to the returned time
$time = $time.addSeconds($seconds) # Add the number of seconds to the returned time
$time = $time.addMillis($millis)   # Add the number of milliseconds to the returned time

Type convert methods

  $id = $id.to18()             # Convert the ID to 18 characters
  $id = $id.to15()             # Convert the ID to 15 characters
$text = $var.toText()          # Convert the variable to a Text value
 $num = $var.toNumber()        # Convert the variable to a Number value
$bool = $var.toBoolean()       # Convert the variable to a Boolean value
$name = $id.idType()           # Return the SObject API name for the ID
$type = $var.type()            # Return type as null, 'Text', 'Number', 'Boolean', 'List', 'Map'

Map methods

 $map = $map.clear()           # Remove all the keys and values from the map
$copy = $map.clone()           # Return a shallow copy of the map
$bool = $map.hasKey($key)      # Return true if map contains specified key
$bool = $map.isEmpty()         # Return true if map contains no keys and no values
$keys = $map.keys()            # Return a list containing all the keys in the map
 $map = $map.putAll($source)   # Copy all keys and values of the source map into the map
 $val = $map.delete($key)      # Delete the map key and return its value
$text = $map.toUrl()           # Return a URL query string from a map of query parameters
$vals = $map.values()          # Return a list containing all values in the map

List methods

$list = $list.add($item)        # Add the item to the end of the list
$list = $list.addAll($items)    # Add all the items to the end of the list
$list = $list.clear()           # Remove all items from the list
$copy = $list.clone()           # Return a shallow copy of the list
$bool = $list.hasItem($item)    # Return true if the list contains the item
 $num = $list.indexOf($item)    # Return the index of the first occurrence of the item
$bool = $list.isEmpty()         # Return true if list the has zero items
$text = $list.join($separator)  # Join the list items into a single text value
$item = $list.remove($index)    # Removes the item at the index and returns it
 $num = $list.size()            # Return the number of items in the list
$list = $list.sort()            # Sort the items in the list 

Examples of maps and lists

Maps associate one or more key-value pairs. A record is an example of a map having many keys (record fields) and associated values. Each value is stored or retrieved using its key. All map keys are text, and map values may consist of any type including another map.

# an empty map
$map = { }

# a map of currency symbol to exchange rate
$gbp_rates = { USD: 1.22, EUR: 1.16, JPY: 167.42 }

# store and retrieve map values
$gbp_rates.CAD = 1.65
$rate = $gbp_rates.CAD

# a map of airline code to airline name
$codename = { AA: 'American Airlines' }

# put() the key in quotes if it contains numbers or special characters
$codename.put('5Y', 'Atlas Air')
$codename.put('?', 'Unknown Airline')

Lists are a collection of items. Items in a list may consist of any type including another list. Use the dot . operator to refer to any item by its index. Unlike a map, items in a list are ordered.

# an empty list
$list = [ ]

# fill a list using a range
$letters = 'a'..'z'
$daysInJanuary = 1..31

# sort a list of prices
$prices = [ 3.50, 1.00, 9.99 ]
$prices.sort()

# get the first item and last item in a price list
$cheapest = $prices.0      # the index 0 is the start of the list
$expensive = $prices.-1    # the index -1 counts from the end of the list

Combine lists and maps to model table rows and columns.

# map of prices
$plans =
{
    BASIC: 9.00
    PREMIUM: 12.50
    ENTERPRISE: 20.00
}

# list of account records
$accounts =
[
    { Name: 'Dickenson Plc',          Code: 'A010',    Plan: 'BASIC' }
    { Name: 'GenePoint',              Code: 'A013',    Plan: 'PREMIUM' }
    { Name: 'Grand Hotels',           Code: 'A024',    Plan: 'ENTERPRISE' }
    { Name: 'Wide World importers',   Code: 'A033',    Plan: 'PREMIUM' }
]

# a list inside a list represents a table
$expectedAnnualRevenues =
[
    [ $accounts.0.Code, $accounts.0.Plan, $plans.get($accounts.0.Plan) * 12 ]
    [ $accounts.1.Code, $accounts.1.Plan, $plans.get($accounts.1.Plan) * 12 ]
    [ $accounts.2.Code, $accounts.2.Plan, $plans.get($accounts.2.Plan) * 12 ]
    [ $accounts.3.Code, $accounts.3.Plan, $plans.get($accounts.3.Plan) * 12 ]
]

Parameters

Parameters change a command's behavior. For example, the Http-Post command is affected by its URL, headers, and body. Parameters can be specified by name, or by order.

# Specify each parameter name followed by its value
Http-Post -url 'example.com' -headers {Cookie:null} -body 'data'

# Alternatively, specify just the values (in the correct order)
Http-Post 'example.com' {Cookie:null} 'data'

Flags are parameters that have no value. The presence (or not) of a flag affects the command. For example Datetime has a -GMT flag. Flag parameters may appear in any order.

# one second before Christmas in GMT time zone
Datetime-Value 2022 12 24 23 59 59 -gmt

# one second before Christmas, user time zone
Datetime-Value 2022 12 24 23 59 59

Aliases are shortcuts to a command that can make logic less verbose. The alias reduces a command to one word and does not affect its behavior.

# one second before Christmas, user time
Datetime 2022 12 24 23 59 59

Pipes

Pipes are commands chained together using the pipe | operator. Pipes feed the data from the left of the pipe | operator into the command on the right. When the data is a list, the command acts on each item one by one. Use pipes to transform and combine lists and maps.

# european taxes
$eu_taxes = [
20, 21, 20, 19, 21, 19, 25, 20, 24,
21, 24, 20, 25, 27, 23, 22, 21, 17,
21, 18, 21, 23, 23, 19, 25, 22, 20]

# the top three european taxes are 27%, 25%, 24%
$top_taxes = $eu_taxes | Unique | Sort | Reverse | Top 3

Special $_ variable refers to the current value being worked on.

Logic params ${...} are procedures you supply for a command to use. For example, the Filter command uses logic to exclude items from a list; the Sort command uses logic to specify what to order by; the Defer command uses logic to do a task later, and so on.

Try# this filter logic will find appointment dates that fall on a sunday
$dates = [ '2022-12-10', '2022-12-11', '2022-12-12' ]
$sundays = Filter $dates $_.dayOfWeek.eq(1) # each item $_ is a date

Commands and pipes can make business logic more readable. Meaningful comments and variable names help too. These two examples produce the same result but in different ways:

Try# Pipe - find odd numbers
$odds = [1, 2, 3, 4, 5] | Filter $_.isOdd

# Loop - find odd numbers (same result)
$odds = []
foreach ($item in [1, 2, 3, 4, 5]) {
    if ($item.isOdd) {
        $odds.add($item)
    }
}

Logic params can take the form of block logic, or concise logic. With concise logic, only a single value is specified, which becomes the implied return value. Block logic uses many statements followed by a return. The above shows a consise body.

Logic params are similar to JavaScript arrow functions.

 

Here is another example with logic blocks, the special variable, pipes and commands. Each list item feeds into the command after a pipe. $_ is the current item being worked on.

# Debit lines used to create an accounting journal
$debits = [
    {Code: 'Rent', Amount: 8800}
    {Code: 'Heat', Amount: 1100}
]

# Pipe Approach - create journal
$credits = $debits
         | Copy
         | Apply ${ $_.Amount *= -1 }
         | Apply ${ $_.Code = 'Bank'}
$journal = $credits + $debits

# Loop Approach - create journal (same result)
$journal = []
foreach ($debit in $debits)
{
    $credit = $debit.clone()
    $credit.Amount *= -1
    $credit.Code = 'Bank'
    $journal.add($credit)
    $journal.add($debit)
}

# net balance
$balance = Sum $journal $_.Amount
Log $balance # zero

Functions

Functions are blocks of re-usable logic. For example, a function can be written to retrieve one exchange rate, then it can be generalized and re-used to retrieve different rates.

Use parentheses after the function name to define comma separated parameters. Use return to hand back the function results. Think of a function as your own custom command.

function Calculate-Age($birthdate)
{
    $today = Script-Today
    return $today.year - $birthdate.year
}

# use the function as you would use a command  
$age = Calculate-Age '1991-02-17'
Log `Edward Sheeran is $age years old`

Handling errors

Errors happen when you try something bad, like dividing by zero. If that happens, catch the error and supply logic to report it, or choose to do something else instead.

Put your happy path logic in a try block, and your error handling logic in a catch block. If an error occurs, the special $_ variable holds the error detail. This is known as an exception.

try
{
    $discount = ($listPrice - $salePrice) / $listPrice
}
catch
{
    throw `Cannot calculate discount: $_`
}

Optionally, the throw keyword allows you to raise your own error with your own message.

Binary data

Streamscript supports the use of binary data. To use binary data in HTTP requests and HTTP responses, use the -base64 flag when running HTTP commands. This flag treats both the request and response body as base64 encoded binary data.

In this example, a PDF invoice is retrieved. The PDF body is stored on the Document Body field as-is, because it was already encoded by the HTTP command using the -base64 flag. Return the $Document record so that Flow Builder can insert it.

# get pdf invoice, base64 encoded
$url = 'www.ups.com/media/en/invoice.pdf'
$pdf = Http-Get $url -base64

# new record
$Document = New-Document {
    Name = 'inv.pdf'
    Body = $pdf.body
}

# output as SObject
return $Document

Alternatively you can store and retrieve plain text in blob fields like Document Body, so long as the Base64-Encode command is used to encode the stored text, or use Base64-Decode to retrieve a binary field back as text.

Bulk/mass actions and directives

Directives must occupy the first line of the script in the form of a comment, for example:

In bulk mode your script will be executed exactly once even though there may be more than one initiating request. For this reason, you must treat merge fields as lists, and you must return a corresponding list. Example:

# Streamscript Bulk
$accounts = {!$Record}
$size = $accounts.size()

# retrieve many cat facts in one callout
$http = Http-Get `catfact.ninja/facts?limit=$size`
$result = Json-Decode $http.body
$facts = $result.data

# put each cat fact on each account
for ($i = 0; $i < $size; $i++)
{
    # use the same list index
    $fact = $facts.get($i)
    $acct = $accounts.get($i)
    $acct.Description = `$fact`
}

When making callouts, we recommend:
- Enable Lightning runtime for flows: Setup > Process Automation Settings
- For screen flows, check: Always start a new transaction (on the step, under advanced)
- For record-triggered flows, check: Run Asynchronously path to access an external system
   after the original transaction for the triggering record is successfully committed.

Appendix - Webhooks

Webhooks are flows that are triggered in response to web requests, as opposed to record changes. With a webhook, data is received from an external application at a site belonging to your Salesforce instance. When events arrive at your URL, the associated flow is triggered with access to the data sent from the external system.

Visit the Integrations tab to set up webhooks:

Read the request data from the $Webhook request properties:

$Webhook.request           # Request body
$Webhook.requestIp         # IP address
$Webhook.requestUri        # Resource URI
$Webhook.requestPath       # Path starting with /
$Webhook.requestMethod     # HTTP request method
$Webhook.requestParams     # Map of URL parameters
$Webhook.requestHeaders    # Map of HTTP request headers

Write the response data to the $Webhook response properties:

$Webhook.response           # Response body
$Webhook.responseStatus     # HTTP status code
$Webhook.responseHeaders    # Map of HTTP response headers

Appendix - Date/Time Formats

Dates and times always use ISO-8601. To display any other format, use the format() method.

# ISO-8601
$date = '2022-12-24'
$time = '23:59:59.999Z'
$datetime = '2022-12-24T23:59:59.999Z'
$ddMMyyyy = $datetime.format('dd/MM/yyyy') # 24/12/2022
Example Format Example Result
"yyyy.MM.dd G 'at' HH:mm:ss z" 2001.07.04 AD at 12:08:56 PDT
"EEE, MMM d, ''yy" Wed, Jul 4, '01
"h:mm a" 12:08 PM
"hh 'o''clock' a, zzzz" 12 o'clock PM, Pacific Daylight Time
"K:mm a, z" 0:08 PM, PDT
"yyyyy.MMMMM.dd GGG hh:mm aaa" 2001.July.04 AD 12:08 PM
"EEE, d MMM yyyy HH:mm:ss Z" Wed, 4 Jul 2001 12:08:56 -0700
"yyMMddHHmmssZ" 010704120856-0700
"yyyy-MM-dd'T'HH:mm:ss.SSSZ" 2001-07-04T12:08:56.235-0700
"yyyy-MM-dd'T'HH:mm:ss.SSSXXX" 2001-07-04T12:08:56.235-07:00
"YYYY-'W'ww-u" 2001-W27-3
Letter Component Presentation Examples
G Era designator Text AD
y Year Year 1996; 96
Y Week year Year 2009; 09
M Month in year Month July; Jul; 07
w Week in year Number 27
W Week in month Number 2
D Day in year Number 189
d Day in month Number 10
F Day of week in month Number 2
E Day name in week Text Tuesday; Tue
u Day number (1 = Monday, 7 = Sunday) Number 1
a Am/pm marker Text PM
H Hour in day (0-23) Number 0
k Hour in day (1-24) Number 24
K Hour in am/pm (0-11) Number 0
h Hour in am/pm (1-12) Number 12
m Minute in hour Number 30
s Second in minute Number 55
S Millisecond Number 978
z Time zone General time zone Pacific Standard Time; PST; GMT-08:00
Z Time zone RFC 822 time zone -0800
X Time zone ISO 8601 time zone -08; -0800; -08:00

Appendix - Regular Expressions

Streamscript uses regular expressions to split and match.

# does a text value match the phone number format
'408-971-2288' match '^\b\d{3}[-.]?\d{3}[-.]?\d{4}\b$'       # true
# extract text items separated by numbers
'aaa123bbb456ccc789'.split('\d+')                            # ["aaa","bbb","ccc"]
Regex Basics Description
^ The start of a string
$ The end of a string
. Wildcard which matches any character, except newline (\n).
| Matches specific character(s) on either side, eg a|b corresponds to a or b
\ Used to escape a special character
a The character "a"
ab The string "ab"
Quantifiers Description
* Used to match 0 or more of the previous (e.g. xy*z could correspond to "xz", "xyz" etc.)
? Matches 0 or 1 of the previous
+ Matches 1 or more of the previous
{5} Matches exactly 5
{5, 10} Matches everything between 5-10
Character Description
\s Matches a whitespace character
\S Matches a non-whitespace character
\w Matches a word character
\W Matches a non-word character
\d Matches one digit
\D Matches one non-digit
[\b] A backspace character
\c A control character
Special Description
\n Matches a newline
\t Matches a tab
\r Matches a carriage return
\ZZZ Matches octal character ZZZ
\xZZ Matches hex character ZZ
\0 A null character
\v A vertical tab
Groups Description
(xyz) Grouping of characters
(?:xyz) Non-capturing group of characters
[xyz] Matches a range of characters (e.g. x or y or z)
[^xyz] Matches a character other than x or y or z
[a-q] Matches a character from within a specified range
[0-7] Matches a digit from within a specified range

Appendix - Wildcard Patterns

Use a template containing wildcard and the like or notlike method to match text

'1234'.like('_23_')    # true
Wildcard Description
_ a placeholder representing any character 
% a placeholder representing any character, any number of times

Appendix - Operators

Boolean Operators Description
 == Equals
 != Not equal
Less Than 
 <= Less than or equal to
 >= Greater than or equal to
 >  Greater than 
 in Is value in a list
 notin Is value missing from a list
 like Like wildcard pattern
 notlike Not like wildcard pattern 
 match Matches regular expression
 notmatch Not matches regular expression 
 && Boolean AND 
 || Boolean OR
 ! Boolean NOT 
Math Operators Description
.. Range
= Assignment
* Multiplier
^ Power
/ Division
% Modulus
+ Addition
- Subtraction
*= Multiply then assign
/= Division then assign
%= Remainder then assign
+= Addition then assign
-= Subtraction then assign
Other Operators Description
++ Increment
-- Decrement
| Pipe
? : Ternary

 

Getting started with Streamscript
Install from the Salesforce AppExchange
Direct install URL: /packaging/installPackage.apexp?p0=04t7F000005N1wy