Skip to main content
Version: Next

Full Example

Our previous examples have been simple and do not begin to show what Opscotch can do.

Let's write a workflow that uses real-world data.

Here is the use case:

We want to continually collect a city's temperature and send that as a metric.

We can use OpenWeatherMap to get weather data. It provides an API that returns the current weather.

Tip

This example is a simple real-world example that demonstrates how the Opscotch runtime works. It only scratches the surface of what you can build.

Getting started

First time here?

You can follow along with this demonstration using your own Opscotch development runtime.

However, if you're feeling overwhelmed, read this page to the end without running the runtime yourself.

When you see how it fits together, follow along with your own Opscotch development runtime.

Prerequisites

Before you start, you'll need a few prerequisites:

  • You'll need a copy of the Opscotch development runtime executable and the Opscotch test runner executable that are compatible with your operating system. See the downloads page
  • We'll use the evergreen opscotch-packager app inside the runtime rather than a separate packager executable.

You should have completed the previous example and successfully run an Opscotch workflow on a runtime. If you have not done that, do it first.

  • We'll be working in your operating system's terminal, so you need to know what that is and how to use it.
  • Create a directory somewhere to work in, and put all the files there, including the Opscotch executables.
  • Ensure you've read the Basic Concepts, specifically the "moving parts".
A note for Windows users
  • Save all text files as UTF-8 with LF line feeds.
  • Any file paths in text files need to either use / instead of \ or escape \ like \\

File setup

In this example, you'll be working with a lot of files. We'll do them one by one, but for your reference, we'll outline the file structure now for you to refer back to:

working directory
|\_ opscotch-runtime
|\_ opscotch-testrunner
|\_ configs (configs for deploying to the runtime)
| \_ weather (weather configs for deploying to the runtime)
| \_ weather.bootstrap.json (weather bootstrap file)
| \_ weather.config.json (weather workflow configuration file)
| \_ weather.oapp (weather packaged workflow file)
|\_ resources (common javascript files)
| \_ weather (weather specific javascript files)
| \_ weather-resultsprocessor.js (weather results processor)
| \_ weather-urlgenerator.js (weather url generator)
\_ tests (tests folder)
|\_ testrunner.config.json (configures the test runner)
\_ weather (weather tests)
|\_ weather.test.json (the weather test file)
|\_ weather.bootstrap.json (the weather test bootstrap file)
|\_ weather.config.json (the weather test configuration file)
\_ weather.response.json (the weather API response file)

Create a folder in the working directory called resources and then one called weather (i.e. resources/weather) - this will be where we store all the javascript files we create.

Create a folder in the working directory called tests and then one called weather (i.e. tests/weather) - this will be where we store the testing files we create.

Linux/macOS:

mkdir -p configs/weather resources/weather tests/weather

Windows:

mkdir configs, resources, tests
mkdir configs\weather, resources\weather, tests\weather

The Weather Data Collection Task

API keys

We'll be pulling our data from OpenWeatherMap. You can get an idea about that here, and you can get your free API key here.

For this example, we'll let you use our API key; however we can't guarantee it will work.

a83da2d308613877a88910dce9568dd5

The URL that we're going to use looks like this, and we'll want to provide the city name and the API key:

https://api.openweathermap.org/data/2.5/weather?units=metric&q={city name}&appid={API key}

And when called effectively returns:

{
"main": {
"temp": 21.55
},
"dt": 1560350645,
"name": "Wellington"
}

Our Opscotch task is as follows:

Every 10 minutes, do this:

  • construct the URL to include the city name and the API key.
  • execute a call to the URL.
  • parse the response into a JSON object.
  • send the temperature as a metric.

Approach

As this is a complete example, we'll take a best-practice approach and do the following:

  • Understand the HTTP call.
  • Create a test.
  • Create a workflow executed against the test.
  • Create a runtime-deployable workflow.

Understanding the HTTP call

The first task in creating a workflow is understanding what needs to be done; this often starts with understanding the HTTP call and the resulting data.

The URL that we're going to use looks like this:

https://api.openweathermap.org/data/2.5/weather?units=metric&q={city name}&appid={API key}

The best thing to do is to execute the HTTP call using your terminal and check out the results:

Linux/macOS:

export OPENWEATHERMAP_KEY=a83da2d308613877a88910dce9568dd5
export CITY_NAME=Wellington
curl "https://api.openweathermap.org/data/2.5/weather?units=metric&q=$CITY_NAME&appid=$OPENWEATHERMAP_KEY"

Windows PowerShell:

$env:OPENWEATHERMAP_KEY = 'a83da2d308613877a88910dce9568dd5'
$env:CITY_NAME = 'Wellington'
Invoke-WebRequest -Uri "https://api.openweathermap.org/data/2.5/weather?units=metric&q=$env:CITY_NAME&appid=$env:OPENWEATHERMAP_KEY"

You should get results like this:

{
"coord": {
"lon": 174.7756,
"lat": -41.2866
},
"weather": [
{
"id": 803,
"main": "Clouds",
"description": "broken clouds",
"icon": "04d"
}
],
"base": "stations",
"main": {
"temp": 12.53,
"feels_like": 11.95,
"temp_min": 12.53,
"temp_max": 15.51,
"pressure": 1016,
"humidity": 81
},
"visibility": 10000,
"wind": {
"speed": 10.29,
"deg": 340
},
"clouds": {
"all": 54
},
"dt": 1666737490,
"sys": {
"type": 2,
"id": 2007945,
"country": "NZ",
"sunrise": 1666718172,
"sunset": 1666767204
},
"timezone": 46800,
"id": 2179537,
"name": "Wellington",
"cod": 200
}

When we review our task, "collect the temperature from a city", we can reduce the JSON to just what we need: the temperature and the timestamp.

{
"main": {
"temp": 12.53
},
"dt": 1666737490
}

Now that we have an understanding of the available data and how to get it, we can start with the tests.

Testing

Testing

Tests are essential: for every workflow you write, you should write a test.

A test means you can be confident that the Opscotch runtime executes consistently, which is especially important during workflow development and upgrades.

The Test Runner Configuration

Let's create the test runner configuration - this tells the test runner how to find our resources and tests: create a file in the tests directory called testrunner.config.json with this content:

{
"resourceDirs": [
"resources"
],
"testDirs": [
"tests"
],
"concurrency" : 1
}
Tip

It is important to remember that all the paths you specify are relative to the working directory where you run the Opscotch executable. Keep this in mind when you encounter errors about missing files.

The Weather Test File

In the tests/weather directory, create a file called weather.test.json - this is where we set up our test for the weather workflow. Take a look at the test schema in the API reference.

Tip

Files named *.test.json are recognized and discovered by the test runner as test files.

Add this content:

{
"fromDirectory": "weather",
"useThisTestBootstrapFile": "weather.bootstrap.json",
"useThisTestConfigFile": "weather.config.json",
"testThisStep" : "current-temperature",
"mockEndpointResponses" : [
{
"whenThisIsCalled" : "http://mockserver/data/2.5/weather?units=metric&q=Wellington&appid=1234",
"returnThisFile" : "weather.response.json"
}
],
"theseMetricsShouldHaveBeenSent": [
{
"key" : "temperature",
"value" : 12.53,
"timestamp" : 1666737490
}
]
}

What we've setup so far:

These files don't exist yet; let's create them.

The weather.bootstrap.json

This Bootstrap construct offers us a few advantages:

  • Defines allowed hosts - security comes first in the runtime, and you are not allowed to call any arbitrary host.
  • Defines allowed HTTP methods and paths.
  • Abstracts the host details from the workflow configuration such that we can use the same configuration with different hosts.
  • Enables testing the workflow configuration without actually using a real server.

The weather.bootstrap.json file is a normal bootstrap file, but in this case it is specific to the test. Because this is a test, we want complete control over the data the runtime receives when it makes an API call. If we let the runtime call the live API, the constantly changing weather data would make testing unreliable. To avoid that, we mock the API response. We start by defining a test bootstrap that uses a host called mockserver instead of the real OpenWeatherMap API server.

Let's do that now: update the contents of tests/weather/weather.bootstrap.json to this:

{
"allowExternalHostAccess" : [
{
"id" : "openweathermap",
"host" : "http://mockserver",
"allowList" : [
{ "method" : "GET", "uriPattern" : "/data/2.5/weather"}
]
}
}
}

This defines:

  • A host record called openweathermap that points to a host http://mockserver (the test runner - this would be https://api.openweathermap.org in the actual Bootstrap file)
  • An allowList that permits an HTTP GET call on the path /data/2.5/weather

The weather.response.json

In the test configuration, we defined mockEndpointResponses to be the following:

{
"mockEndpointResponses" : [
{
"whenThisIsCalled" : "http://mockserver/data/2.5/weather?units=metric&q=Wellington&appid=1234",
"returnThisFile" : "weather.response.json"
}
]
}

When the runtime calls http://mockserver/data/2.5/weather?units=metric&q=Wellington&appid=1234 during the test, the contents of weather.response.json are returned. Because this is an array, you can define any number of these responses.

Notice that we refer to http://mockserver - which comes from the test Bootstrap.

Remember that we're pretending (mocking) to be the OpenWeatherMap API server - the contents of weather.response.json should be the contents of what the OpenWeatherMap API server would return. However, if we set the content to be the trimmed-down version (i.e. only what we want to use), then we're also giving a clue to anyone reading the tests about the data that we care about: so let's set the content of tests/weather/weather.response.json to this:

{
"main": {
"temp": 12.53
},
"dt": 1666737490
}

The Weather Metric

In the test configuration file, we defined this:

{
"theseMetricsShouldHaveBeenSent": [
{
"key" : "temperature",
"value" : 12.53,
"timestamp" : 1666737490
}
]
}

This declares that when the test runs, we expect the runtime to send a metric with a key of temperature, a value of 12.53, and a timestamp of 1666737490. That timestamp comes from the dt field in the OpenWeatherMap API response.

If it does not happen the test will fail - its up to us to make this happen in the workflow!

Tip

You don't need to define the expected timestamp because sometimes you won't know - for the curious there are ways around this.

The Weather Workflow

Now we're done (for now) with the test plumbing, and we've defined our expectation (the metric) we can work on the workflow to do all the work (exciting)!

Always keep the goal in mind:

We want to continually collect a city's temperature and send that as a metric.

Take a look at the workflow structure that we need to work in:

Let's give it a go: set the contents of tests/weather/weather.config.json:

{
"workflows": [
{
"name" : "Current Temperature",
"steps" : [
{
"stepId" : "current-temperature",
"trigger" : {
"timer" : {
"period" : 60000
}
},
"urlGenerator" : {
"script" : ""
},
"resultsProcessor" : {
"script" : ""
}
}
]
}
]
}

So far, we've defined the following

  • A workflow with a friendly name.
  • A step with an ID and a timer.
  • An empty urlGenerator and resultsProcessor.

At this stage, we should be able to run the test and have it fail... why? Because we've not met the expectation of the metric being sent, we've not even called the URL!

Let's live on the edge and learn how to run the test even though it will fail.

Running the (failing) test

Running the test involves two processes:

  • The test runner.
  • The Opscotch runtime.
Tip

You'll need to be able to run both these at the same time in different terminal windows.

In the first terminal, in your working directory, run the test runner with the tests/testrunner.config.json file:

opscotch-testrunner tests/testrunner.config.json

On Windows, use opscotch-testrunner.exe.

Executing this will start the test runner, and you'll see a log line similar to this:

Start the Opscotch runtime with these arguments: /home/jeremy/testing/test.bootstrap.json LS0t...S0tLS0

These arguments are the path to the test runner bootstrap and the test runner private key, so the runtime can decrypt the bootstrap.

Copy the arguments and start the Opscotch development runtime in the other terminal window. You can leave the runtime running:

opscotch-runtime <both the arguments>

On Windows, use opscotch-runtime.exe.

Executing the runtime command, you'll see these key logs:

New configuration loaded
Agent initialisation complete
Activating Workflows
opscotch test suite: READY

When the test runner sees the opscotch test suite: READY log, it starts running the test and outputs these key logs:

opscotch runtime READY, starting tests 
100.0% Complete; Running: 0; Success: 0; Failed: 1
Test Finished: [0][FAILED] weather.test.json
Here are the logs:
Waiting for test assertions
HP3: Url not set for native-test-0-d2e21577-current-temperature
Test timed out waiting for metrics, requests, after 10 seconds
Expected HTTP GET http://localhost:10001/data/2.5/weather?units=metric&q=Wellington&appid=1234

This clearly says that the test expected the URL to be called and it was not.

Let's fix that.

Implement the urlGenerator

Let's think about how the urlGenerator might work:

  • As defined in the test, this is what we expect to be called:
    • http://mockserver/data/2.5/weather?units=metric&q=Wellington&appid=1234
      • broken down:
        • http://mockserver is the host that's defined in the Bootstrap by the named host openweathermap
        • /data/2.5/weather?units=metric&q=Wellington&appid=1234 is the path and the query string

There are a few ways we can tackle this:

  • We can hard code the URL 'as is' - this would work fast, but it's not very flexible if we want to reuse this workflow.
  • We can parameterise the city name and appid arguments - this will take a bit longer but makes the workflow reusable.

Let's do both (nothing like a bit of instant gratification to pique one's interest).

Hardcoded URL

Adding a hardcoded urlGenerator would look like this (update tests/weather/weather.config.json):

{
"workflows": [
{
"name" : "Current Temperature",
"steps" : [
{
"stepId": "current-temperature",
"trigger" : {
"timer" : {
"period" : 60000
}
},
"urlGenerator" : {
"script" : "context.setUrl('openweathermap','/data/2.5/weather?units=metric&q=Wellington&appid=1234');"
},
"resultsProcessor" : {
"script" : ""
}
}
]
}
]
}

urlGenerator is a JavascriptProcessor, and more specifically, a JavaScriptSource. It lets you define either a JavaScript resource or a JavaScript script.

To interact with the runtime from JavaScript, you use the JavascriptContext, which is referred to as context in JavaScript. In the example above, the urlGenerator.script property is a JavaScript string that calls context.setUrl(hostref, path). The hostref is the named host in the bootstrap, and the path is the URL path to call.

Tip

You cannot enter any arbitrary URL or host. Opscotch is secure by design, and the person controlling the bootstrap is the only one who can authorize which hosts can be called.

Re-run the tests and see what happens:

opscotch-testrunner tests/testrunner.config.json

This time the test runner log outputs:

Test Finished: [FAILED] [1] file=tests/weather/weather.test.json: Message with body {"token":"native-test","timestamp":0,"name":"temperature","value":12.53,"dimensionMap":{},"type":"metric"} was expected

And further in the logs:

Responding to /data/2.5/weather?units=metric&q=Wellington&appid=1234 with /home/jeremy/testing/tests/weather/weather.response.json
Test timed out waiting for metrics, after 10 seconds
Metric with body {"token":"native-test-0","timestamp":1666737490,"name":"temperature","value":12.53,"dimensionMap":{},"type":"metric"} was expected

We can see that the HTTP request was made by the runtime to the testing harness, which responded with the correct file.

Next is the log saying that the metric was expected and not received. Let's continue with the instant gratification and implement metric sending.

Metric sending

At this point, we have constructed the URL and asked the runtime to execute the HTTP call. We can now implement the resultsProcessor: the runtime passes us the HTTP response, and we send a metric to the test runner.

In our test, we declared the expected metric:

{
"theseMetricsShouldHaveBeenSent": [
{
"key" : "temperature",
"value" : 12.53,
"timestamp" : 1666737490
}
]
}

Take a look at the JavascriptContext and find the sendMetric() method. There are several versions. The most basic is sendMetric(key, value). With that version, you do not supply a timestamp; the runtime creates one for you. The next version includes a timestamp: sendMetric(timestamp, key, value). That is the better choice here because the data already includes a timestamp.

Let's think about how we'll process the response from the runtime. The response comes from the OpenWeatherMap API server, although in this test it is actually mocked by the test runner. It passes us this:

{
"main": {
"temp": 12.53
},
"dt": 1666737490
}

We'll use the context method getMessageBodyAsString(), which, as it says, returns a string:

body = context.getMessageBodyAsString();

To create our metric using the method described above, we need the value and the timestamp. Since body is currently a JSON string, it makes sense to parse it into a JSON object using the standard JavaScript JSON.parse() method:

body = context.getMessageBodyAsString();
jsonObject = JSON.parse(body);

Now we can use JavaScript object notation to extract the value and timestamp into variables:

body = context.getMessageBodyAsString();
jsonObject = JSON.parse(body);
timestamp = jsonObject.dt;
value = jsonObject.main.temp;

We're almost ready to send our metric - just one more thing: what is the key? In the test, we said "temperature", - so that's the key we'll set. Let's do it:

body = context.getMessageBodyAsString();
jsonObject = JSON.parse(body);
timestamp = jsonObject.dt;
value = jsonObject.main.temp;
key = "temperature";

context.sendMetric(timestamp, key, value);

Done! That was easy.

If you're coding along, you're probably wondering: "where do I put all that?" - you have two options:

  1. Directly on the script property of the resultsProcessor.
  2. In a file references by the resource property of the resultsProcessor.

As we've already demonstrated using the script property, we'll use the resource property - arguably the correct thing to do as this is more than a simple one-liner.

Create a file in resources/weather/weather-resultsprocessor.js and update the contents to the script above (the one with the sendMetric command!)

Update tests/weather/weather.config.json with the resource set on the resultsProcessor:

{
"workflows": [
{
"name" : "Current Temperature",
"steps" : [
{
"stepId": "current-temperature",
"trigger" : {
"timer" : {
"period" : 60000
}
},
"urlGenerator" : {
"script" : "context.setUrl('openweathermap','/data/2.5/weather?units=metric&q=Wellington&appid=1234');"
},
"resultsProcessor" : {
"resource" : "weather/weather-resultsprocessor.js"
}
}
]
}
]
}

Re-run the tests and see what happens:

opscotch-testrunner tests/testrunner.config.json

This time the test runner log outputs:

Test Finished: [0][SUCCESSFUL] /home/jeremy/testing/tests/weather/weather.test.json

🎉 🎉 CONGRATULATIONS 🎉🎉

You've successfully created a workflow that meets the criteria of your test!

Can we make this better? Remember the hardcoded URL? Ick. Let's do that properly.

Parameterised URL

Remember this classy piece of instant gratification:

{
"urlGenerator" : {
"script" : "context.setUrl('openweathermap','/data/2.5/weather?units=metric&q=Wellington&appid=1234');"
}
}

We hard coded this q=Wellington&appid=1234 which, if we want this workflow to be flexible and reusable, we should make the city name and appid into parameters.

In Opscotch, the data construct lets you pass properties into your JavaScript. Take a look at the bootstrap and workflow schemas. You will see multiple "data": {} blocks throughout the configuration. That indicates that you can provide a data object that is passed into your JavaScript and made available through context.getData() for the full object or context.getData(key) for a single named property.

Data objects are inherited and merged, so if any object in the configuration tree declares a data object, it becomes available to descendant JavaScript. This also applies to the bootstrap: if you declare a data object on the bootstrap, it is available to any JavaScript executed in the workflows loaded by that bootstrap.

How can we use this to parameterise the URL?

We can enhance our urlGenerator: add the data object with the city name and appid:

{
"urlGenerator" : {
"script" : "context.setUrl('openweathermap','/data/2.5/weather?units=metric&q=Wellington&appid=1234');",
"data" : {
"cityname" : "Wellington",
"appid" : "1234"
}
}
}

We'll now modify our script to get the parameters:

cityname = context.getData("cityname");
appid = context.getData("appid");

Then we'll use those to generate our URL:

cityname = context.getData("cityname");
appid = context.getData("appid");

context.setUrl('openweathermap','/data/2.5/weather?units=metric&q=' + cityname + '&appid=' + appid);

As our script is not a simple one-liner, let's throw it into a resource file: Create a file in resources/weather/weather-urlgenerator.js and update the contents to the script above.

Then update the workflow:

{
"workflows": [
{
"name" : "Current Temperature",
"steps" : [
{
"stepId": "current-temperature",
"trigger" : {
"timer" : {
"period" : 60000
}
},
"urlGenerator" : {
"resource" : "weather/weather-urlgenerator.js",
"data" : {
"cityname" : "Wellington",
"appid" : "1234"
}
},
"resultsProcessor" : {
"resource" : "weather/weather-resultsprocessor.js"
}
}
]
}
]
}

Re-run the tests and see what happens:

opscotch-testrunner tests/testrunner.config.json

The test runner log outputs:

Test Finished: [0][SUCCESSFUL] /home/jeremy/testing/tests/weather/weather.test.json
Tip

Did it actually work? Try changing the appid and re-running the test - it should fail.

It is always good to double-check.

Good Job! However, simply adding the data object to the urlGenerator doesn't make the workflow reusable - the city name and the appid are still hardcoded into the workflow: let's move the data object onto the Bootstrap.

Update the contents of tests/weather/weather.bootstrap.json to this:

{
"allowExternalHostAccess" : [
{
"id" : "openweathermap",
"host" : "http://mockserver",
"allowList" : [
{ "method" : "GET", "uriPattern" : "/data/2.5/weather"}
]
}
],
"data" : {
"cityname" : "Wellington",
"appid" : "1234"
}
}

Then update the workflow to remove the data object:

{
"workflows": [
{
"name" : "Current Temperature",
"steps" : [
{
"stepId": "current-temperature",
"trigger" : {
"timer" : {
"period" : 60000
}
},
"urlGenerator" : {
"resource" : "weather/weather-urlgenerator.js"
},
"resultsProcessor" : {
"resource" : "weather/weather-resultsprocessor.js"
}
}
]
}
]
}

Re-run the tests and see what happens:

opscotch-testrunner tests/testrunner.config.json

The test runner log outputs:

Test Finished: [SUCCESSFUL] [1] file=tests/weather/weather.test.json

Well done!! You've (low) coded up a bootstrap, workflow and test that does... something!

Let's move on to setting this up to run in a runtime without the testing harness.

Even less hard coded

While it might seem that we've still hardcoded the city and appid properties, they're not in our code or workflows, they're in the bootstrap, which is configuration set by the installer, so we might say they've been "hard-configured".

If you want even less hard-coding, you could use an environment variable in the bootstrap and have the value set at runtime rather than install time.

Deploying to the runtime

Now we have a tested workflow and a bootstrap example, so we can set up the runtime to execute the workflow.

Quickly review the "hello world" example, where you started the development runtime. Remind yourself of:

  • The runtime bootstrap.
  • The keys.
  • The packager.

Let's start with the workflow. Copy tests/weather/weather.config.json to configs/weather/weather.config.json: Linux/macOS:

cp tests/weather/weather.config.json configs/weather/weather.config.json

Windows:

copy tests/weather/weather.config.json configs/weather/weather.config.json

Now create the file configs/weather/weather.bootstrap.json with both the weather deployment and the evergreen packager app:

[
{
"deploymentId" : "weather-examples",
"remoteConfiguration" : "configs/weather/weather.config.json",
"allowExternalHostAccess" : [
{
"id" : "openweathermap",
"host" : "https://api.openweathermap.org",
"allowList" : [
{ "method" : "GET", "uriPattern" : "/data/2.5/weather" }
]
}
],
"data" : {
"cityname" : "Wellington",
"appid" : "a83da2d308613877a88910dce9568dd5"
}
},
{
"deploymentId": "opscotch-packager",
"remoteConfiguration": "https://github.com/opscotch/opscotch-apps/releases/download/opscotch-packager-latest/opscotch-packager.oapp",
"persistenceRoot": "persistence",
"allowHttpServerAccess": [
{
"id": "api",
"port": 22222
}
],
"packaging": {
"packageId": "opscotch-packager",
"packagerIdentities": [
"opscotch-packager-identiy"
],
"requiredSigners": [
{
"id": "opscotch-app",
"description": "Official opscotch app"
},
{
"id": "opscotch-packager-app",
"description": "Official opscotch Packager"
},
{
"id": "opscotch-packager-1.x",
"description": "Opscotch Packager v2 version 1.x lock"
}
]
},
"keys": [
{
"id": "opscotch-packager-identiy",
"type": "public",
"purpose": "authenticated",
"keyHex": "D1F8F5CD6424A638A938903EBEA7C140A678873417E5642A3993D2C1BE74A01E"
},
{
"id": "opscotch-app",
"type": "public",
"purpose": "sign",
"keyHex": "8C9AED01FF5E6695E4464E754697F22D653A9FB35E1233627D81D91F89CF2874"
},
{
"id": "opscotch-packager-app",
"type": "public",
"purpose": "sign",
"keyHex": "6D9841078C83239B1CF148BF92A533A4C8BA4901DA0AD1ADDE2FE2D23BA3F156"
},
{
"id": "opscotch-packager-1.x",
"type": "public",
"purpose": "sign",
"keyHex": "5912C087D6C958A84433F666784CE228B17EF34E39E5D9C7FD2BBA5927A760F9"
}
]
}
]

Start the development runtime with the bootstrap file:

opscotch-runtime --accept-legal=yes configs/weather/weather.bootstrap.json

On Windows, use opscotch-runtime.exe.

This starts both the weather workflow and the packager API on port 22222. The packager entry includes the published signing and authentication keys for the evergreen packager app, so the runtime can verify the downloaded app before loading it.

Package the workflow from another terminal:

curl "http://localhost:22222/workflow-package" \
-H "Accept: application/octet-stream" \
-F 'body={ "packageId": "weather-examples" }' \
-F 'stream=@configs/weather/weather.config.json;type=application/octet-stream' \
--output configs/weather/weather.oapp

If you want to generate your own signing or encryption keys for real deployments, use the packager /key-gen endpoint. This tutorial keeps the workflow package simple and does not add custom signing or encryption.

Now update the existing weather-examples entry in configs/weather/weather.bootstrap.json so it loads the packaged file instead of the raw JSON:

[
{
"deploymentId" : "weather-examples",
"remoteConfiguration" : "configs/weather/weather.oapp",
"packaging" : {
"packageId" : "weather-examples"
},
"allowExternalHostAccess" : [
{
"id" : "openweathermap",
"host" : "https://api.openweathermap.org",
"allowList" : [
{ "method" : "GET", "uriPattern" : "/data/2.5/weather" }
]
}
],
"data" : {
"cityname" : "Wellington",
"appid" : "a83da2d308613877a88910dce9568dd5"
}
},
{
"deploymentId": "opscotch-packager",
"remoteConfiguration": "https://github.com/opscotch/opscotch-apps/releases/download/opscotch-packager-latest/opscotch-packager.oapp",
"persistenceRoot": "persistence",
"allowHttpServerAccess": [
{
"id": "api",
"port": 22222
}
],
"packaging": {
"packageId": "opscotch-packager",
"packagerIdentities": [
"opscotch-packager-identiy"
],
"requiredSigners": [
{
"id": "opscotch-app",
"description": "Official opscotch app"
},
{
"id": "opscotch-packager-app",
"description": "Official opscotch Packager"
},
{
"id": "opscotch-packager-1.x",
"description": "Opscotch Packager v2 version 1.x lock"
}
]
},
"keys": [
{
"id": "opscotch-packager-identiy",
"type": "public",
"purpose": "authenticated",
"keyHex": "D1F8F5CD6424A638A938903EBEA7C140A678873417E5642A3993D2C1BE74A01E"
},
{
"id": "opscotch-app",
"type": "public",
"purpose": "sign",
"keyHex": "8C9AED01FF5E6695E4464E754697F22D653A9FB35E1233627D81D91F89CF2874"
},
{
"id": "opscotch-packager-app",
"type": "public",
"purpose": "sign",
"keyHex": "6D9841078C83239B1CF148BF92A533A4C8BA4901DA0AD1ADDE2FE2D23BA3F156"
},
{
"id": "opscotch-packager-1.x",
"type": "public",
"purpose": "sign",
"keyHex": "5912C087D6C958A84433F666784CE228B17EF34E39E5D9C7FD2BBA5927A760F9"
}
]
}
]

Because the bootstrap changed, restart the development runtime:

opscotch-runtime --accept-legal=yes configs/weather/weather.bootstrap.json

Look out for logs like:

Bootstrap configuration loaded
Agent initialisation complete
Activating Workflows
Starting: weather-examples-...-current-temperature/...
Remote call to: https://api.openweathermap.org/data/2.5/weather?units=metric&q=Wellington&appid=a83da2d308613877a88910dce9568dd5
Registered new metric temperature
Metric Sender not defined. Metric generated: {"token":"weather-examples","timestamp":1740368742,"name":"temperature","value":22.81,"dimensionMap":{"configId":"...","deploymentId":"weather-examples","ori":"...","ov":"3.0.0-SNAPSHOT-dev"},"type":"metric"}

This is it. It worked. We can see that the runtime started the workflow, made an HTTP call to api.openweathermap.org, and created a metric from the response.

You'll have noticed the last log: Metric Sender not defined. In this case, that is OK, because we have not told the runtime where to send metrics. That is outside the scope of this tutorial because it would require another service in addition to Opscotch, and we want to keep this example simple.

Remember to shutdown the opscotch runtime that you started using ctrl-c.