Reading width
Wide uses the full column for everything, text, diagrams, code, and exercises. Narrow keeps the standard reading width.
Text size
Scales the body text. Headings and code blocks keep their size.
In this section
0.5 Start Here: Your First Run
Introduction
This is the first workflow you build, and the smallest one that uses every part of the lab. It is started by an HTTP request, makes one HTTP call to the harness, and sends back a reply.
Along the way it introduces the three things every lesson after this one depends on: a workflow definition, which is JSON; an HTTP exchange, which is how the workflow talks to every service; and the run history, which is how you find out what a workflow did.
Each step is shown with the run the course made, in the local runtime against the harness, so you can compare yours line by line. Two runs are shown: one that worked, and one broken on purpose, because reading a failure is the skill you will use most.
By the end you will have a working project, a workflow that runs, and a way to see exactly what it did, which is everything Module 1 assumes.
A Project and a Workflow
Four files to startWith the lab running from Lesson 0.4, create the project in Visual Studio Code. Learn's steps: close any open folder, open the Azure pane, and from the Workspace toolbar's Azure Logic Apps menu choose Create new logic app workspace.
Pick an empty folder, give the workspace a name, choose the Logic app project template, and when asked for a workflow template choose Stateful Workflow and name it first-run. The extension creates a project like this:
{
"first-lab/": {
"host.json": "the runtime's settings: the workflow engine bundle",
"local.settings.json": "this machine's settings: storage, harness address",
"parameters.json": "the workflow parameters and their values",
"first-run/": {
"workflow.json": "the workflow definition"
}
}
}
host.json is the file from Lesson 0.3, naming the workflow engine. local.settings.json holds this machine's settings: the storage connection that points at Azurite and, once you add it, the harness's address. parameters.json holds values the workflows refer to by name, and each workflow lives in a folder of its own, with its definition in workflow.json.
A project is the unit that runs. The runtime started in this folder runs every workflow in it, and in Module 11 the same folder, zipped, is what is deployed to Azure. Everything the course builds is added to this project or one like it, a workflow folder at a time.
That is why the course keeps one project per playbook rather than one per lesson: the triage playbook you start in Module 1 is the same folder you deploy in Module 11, with workflows added to it along the way.
The workspace the extension creates around the project is a Visual Studio Code workspace file and a folder. You can ignore the workspace file; open the project through it so the extension recognizes the folder as a logic app project, and the Run menu, the Overview page and the designer all work.
Learn's steps also ask, at one point, whether to use connectors from Azure. Choose to skip it. Connectors from Azure need an Azure account and are only used by the Sentinel trigger in Module 11; everything before that uses built-in operations, which run in the runtime itself.
Once the project exists, add one line to local.settings.json, inside Values: HarnessUrl, with the harness's address from Lesson 0.4. Section 4 explains why the address lives there and not in the workflow. local.settings.json is never deployed and should never be committed to source control, because from Module 1 it also holds the lab's client secret.
The Definition Is JSON
Objects, arrays, strings and numbersOpen first-run/workflow.json and replace its contents with the workflow below. This is the whole definition, with the schema address shortened so it fits the page:
{
"definition": {
"$schema": "https://schema.management.azure.com/.../workflowdefinition.json#",
"contentVersion": "1.0.0.0",
"triggers": {
"manual": {
"type": "Request",
"kind": "Http",
"inputs": {}
}
},
"actions": {
"Ask_the_harness": {
"type": "Http",
"runAfter": {},
"inputs": {
"method": "GET",
"uri": "@{parameters('harnessUrl')}/_harness/assumptions"
}
},
"Reply": {
"type": "Response",
"kind": "Http",
"runAfter": {
"Ask_the_harness": [
"Succeeded"
]
},
"inputs": {
"statusCode": 200,
"body": {
"hello": "@triggerBody()?['name']",
"harnessStatus": "@outputs('Ask_the_harness')?['statusCode']"
}
}
}
},
"outputs": {},
"parameters": {
"harnessUrl": {
"type": "String"
}
}
},
"kind": "Stateful"
}
JSON is a way of writing structured data as text, and it has very few rules. Curly braces hold an object: a set of named values, each written as a name in double quotes, a colon and the value, separated by commas.
Square brackets hold an array: a list of values in order. A value is a string in double quotes, a number, true, false, null, or another object or array. That is all of it, and every file in this course that ends in .json follows those rules.
The rules are strict in ways that catch people early. Names must be in double quotes, single quotes are not allowed, and a comma after the last member of an object or array makes the file invalid. If the runtime refuses to load a workflow, a stray comma or a missing quote is the first thing to look for, and the editor underlines both.
Read the definition from the outside in. The outermost object has two members: definition, the workflow itself, and kind, which is Stateful. Inside definition are the schema it follows, its parameters, its triggers, its actions and its outputs. The trigger, called manual, is what starts a run. The two actions are what a run does.
Each action has a type, which says what kind of step it is; inputs, which say what it does; and runAfter, which says when. Ask_the_harness has an empty runAfter, so it runs first. Reply runs after Ask_the_harness has Succeeded. A workflow is a set of steps and the order between them, written as data.
Strings that begin with an at sign are expressions: small formulas the runtime evaluates when the action runs, such as one that reads the trigger's request body. An expression inside a longer string is wrapped in at-sign braces, as in the harness address. They are how a workflow uses data that only exists at run time. The course introduces them one at a time from here on.
The Trigger
An address that starts a runThe workflow is started by a Request trigger, which takes no inputs at all in this first version:
{
"triggers": {
"manual": {
"type": "Request",
"kind": "Http",
"inputs": {}
}
}
}
A Request trigger gives the workflow an address. When the runtime starts, it creates an endpoint for the trigger, and an HTTP request sent there starts a run. The kind Http and the empty inputs mean any request with any body is accepted; Module 1 adds a schema that rejects requests that are not shaped like an incident.
The name manual is the default the designer gives a Request trigger, and the address uses it, so renaming the trigger changes the URL. The course keeps the default in every workflow. The workflow's name, first-run, is the folder's name, so the same rule applies to it: rename the folder and the address changes with it, and any caller holding the old URL starts nothing.
Start the runtime with Start Debugging on the Run menu. The terminal shows the runtime starting and, after a few seconds, the workflow being loaded. Then, as Learn describes, right-click workflow.json and choose Overview. The Overview page shows the trigger's callback URL:
http://localhost:7071/api/first-run/triggers/manual/invoke?api-version=2022-05-01&sp
=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=<signature>
The URL has the form Learn gives: the runtime's address on port 7071, the workflow's name, the trigger's name, and a query string. The query string ends with a signature, shown here masked, which is what lets the runtime accept the request without any other authentication.
Anyone with the full URL can start the workflow, which is why the course never writes a signature into a comment, a log or a lesson, and why every case since Module 3 checks that no comment contains one.
The query string in the course's URL has four members: api-version, 2022-05-01; sp, the permission the signature grants, here /triggers/manual/run, which allows running this trigger and nothing else; sv, the signature's version, 1.0; and sig, the signature itself.
Copy the whole URL from the Overview page each time the runtime starts rather than retyping it, because a single wrong character in the signature is enough for the request to be refused, and a refused request never becomes a run you can read.
In the lab the callback URL only works on your own machine, so the signature protects nothing that matters yet. Treat it as if it did anyway, because the habit carries over to the deployed workflow, where a leaked URL is a way to start a playbook from anywhere. In Module 11 the incident trigger replaces this one, and Sentinel, not a URL, starts the run.
One HTTP Call
Method, address, statusThe first action calls the harness and asks it for the list of assumptions it serves. It is the whole of the workflow's outbound work:
{
"Ask_the_harness": {
"type": "Http",
"runAfter": {},
"inputs": {
"method": "GET",
"uri": "@{parameters('harnessUrl')}/_harness/assumptions"
}
}
}
HTTP is how almost everything in this course talks: the workflow to the harness, and later to Defender, Graph and Sentinel. A request has a method, an address and, optionally, headers and a body. GET asks for something, POST sends something, PATCH changes part of something, DELETE removes it.
The reply has a status code, headers and, usually, a body. Status codes in the 200s mean success, the 400s mean the request was wrong or not allowed, and the 500s mean the service failed.
Those three families decide most of what a playbook does next. A 200 means use the body. A 401 or 403 means the identity or its permissions are wrong, and retrying will not help. A 429 or a 503 means the service is busy, and waiting will. Module 4 is built on that difference; here it is enough to recognize the families.
This action sends a GET to the harness's assumptions list, the same address Lesson 0.4 used to check that the harness was up. It needs no token, because the list is the harness's own, not a Microsoft API; from Module 1, calls to the Microsoft APIs carry a token in an Authorization header.
The address is built from a parameter, harnessUrl, rather than written into the action. The parameter gets its value from the app setting added in Section 1:
{
"parameters.json": {
"harnessUrl": {
"type": "String",
"value": "@appsetting('HarnessUrl')"
}
},
"local.settings.json, in part": {
"HarnessUrl": "http://127.0.0.1:8642"
}
}
parameters.json declares the parameter and says its value is the app setting HarnessUrl; local.settings.json gives the app setting its value on this machine. It is two steps instead of one so that the definition never contains an address. In Module 11 the same definition is deployed with the setting pointing at the real services, and nothing in workflow.json changes.
The same split explains a mistake that is easy to make in the first week. Edit local.settings.json while the runtime is running and the workflow keeps using the old value, because settings are read when the runtime starts.
Stop debugging and start again after any change to settings or parameters. A change to workflow.json, by contrast, is the thing the extension is built around, and the designer and the run history both show the workflow as it now stands.
The Response
What the caller gets backThe second action replies to whoever sent the request, using what the first action got back:
{
"Reply": {
"type": "Response",
"kind": "Http",
"runAfter": {
"Ask_the_harness": [
"Succeeded"
]
},
"inputs": {
"statusCode": 200,
"body": {
"hello": "@triggerBody()?['name']",
"harnessStatus": "@outputs('Ask_the_harness')?['statusCode']"
}
}
}
}
A Response action ends the request: its status code and body are what the caller receives. The body here is an object with two members, each an expression.
The first reads the name member from the request's body, with the question mark meaning that a missing member gives null instead of an error. The second reads the status code from the first action's outputs, which is how one step uses what another produced.
The question mark matters more than it looks. Without it, a request with no name member fails the expression, and the Response fails with it. With it, the reply simply carries null. Most of the expressions in the playbook read data that may be absent, and the question mark is how they say so.
Send the workflow a request with curl, or any HTTP tool, using the callback URL from the Overview page. The last line below is the reply:
curl -s -X POST '<callback URL>' \
-H 'Content-Type: application/json' \
-d '{"name": "analyst"}'
{"hello": "analyst", "harnessStatus": 200}
The reply names the caller back and reports that the harness answered with 200. That is the course's run: sent one request with the name analyst, it answered exactly that. By the run's own start and end times the whole run took 1.4 seconds, most of it the runtime starting the run rather than the call to the harness.
A workflow does not have to reply. Most of the playbook's work happens after it has replied, and Module 1 explains why the triage workflow sends its Response early and carries on. Here the reply is simply the easiest way to see that the run worked, and the easiest thing to compare with the course's run: same request, same reply, field for field.
Try two variations before moving on. Send a body with no name member and the reply carries null where the name was. Send a name that is a number, and the reply carries the number, because nothing yet says what a body must look like. Module 1's schema is what turns a badly shaped body into a rejection at the door, before any step runs.
If the reply never comes, or comes back as an error, the run is still recorded, and the next section is where to look.
Reading the Run
What the runtime recordedBecause the workflow is stateful, the runtime recorded the run. Learn's steps: reopen the workflow's Overview page, find the run under Run history, and select its identifier to see its steps. The course's run, read back through the runtime's run history:
{
"run": {
"id": "08584070057853254325679271784CU00",
"status": "Succeeded",
"trigger": "manual"
},
"actions": [
{
"name": "Ask_the_harness",
"status": "Succeeded",
"code": "OK",
"ms": 236
},
{
"name": "Reply",
"status": "Succeeded",
"code": "OK",
"ms": 64
}
]
}
The run has an id, a status and the trigger that started it. Each action has a status, a code and a duration in milliseconds. Succeeded with the code OK, for both, means each did what it was asked. A failed action would show Failed, and the actions after it would show Skipped, because their runAfter asked for Succeeded.
Select an action to see its inputs and outputs, which Learn's page shows with options for raw inputs and raw outputs. For Ask_the_harness:
{
"Ask_the_harness, inputs": {
"uri": "http://127.0.0.1:8642/_harness/assumptions",
"method": "GET"
},
"outputs, in part": {
"statusCode": 200,
"headers": {
"x-harness": "au-harness/1.0 (not a Microsoft service)",
"Content-Type": "application/json"
},
"body": "44 assumptions, by id"
}
}
The inputs are exactly what was sent: the method and the address, with the parameter already replaced by its value. The outputs are exactly what came back: the status code, the headers, including the one that says the harness is not a Microsoft service, and the body, here 44 assumptions listed by id.
When a workflow does something you did not expect, this is where you find out why: the inputs show what it actually sent, and the outputs show what it actually received.
This is the habit the course asks for in every lesson. Before deciding a workflow is wrong, open the run and read the inputs of the step that misbehaved. More often than not, the step did exactly what it was told, and what it was told came from an earlier step or a setting.
The run id is also how the course refers to a run. Every lesson that shows a run gives its id, and the run files the course keeps carry the same id, so a figure in a lesson can always be traced back to the run that produced it. Yours will have different ids and times, and the same statuses and codes.
Learn notes that run history appears only while the logic app is running, so keep the runtime up while you read it. The history itself is kept in Azurite and survives restarts until you delete Azurite's folder.
A Run That Fails
And what changes in Module 1Before moving on, break the workflow on purpose and read the result. The course changed the harness address to a port nothing listens on, restarted the runtime and sent the same request. What came back, and what the run history recorded:
{
"reply to the caller": {
"status": 502,
"code": "NoResponse"
},
"run": "Failed",
"Ask_the_harness": {
"status": "Failed",
"code": "BadRequest",
"seconds after the first try, per try": [
0.0,
5.8,
18.8,
33.8
],
"lasted, seconds": 74
},
"Reply": {
"status": "Skipped",
"code": "ActionSkipped"
}
}
The caller got a 502 with the code NoResponse, not a reply from the workflow, because the Response never ran. In the run history, Ask_the_harness failed and Reply was skipped, exactly as its runAfter said it would be. The failed action's error names the cause:
HTTP request failed as there is an error: 'Connection refused (127.0.0.1:8699)'.
The action was tried 4 more times after the first, the tries further apart each time, and failed for 74 seconds before giving up.
That is the default retry policy at work: Learn describes it, for most operations, as an exponential interval policy that sends up to 4 retries when a request times out or fails. Nothing in the definition asked for it, which is why Module 4 sets every retry policy explicitly.
Two details in the record are worth noticing. The caller's error came from the runtime, not from the workflow, so a caller cannot tell a refused connection from any other failure; only the run history can. And Reply's status is Skipped, not Failed: it never ran, so it has no inputs or outputs to read.
When a run fails, the first Failed action in order is where the reason is, and every Skipped action after it is a consequence.
Put the address back before going on. That exercise is the course in miniature: a change, a run, a record of what happened, and a reason found in the record rather than guessed.
Module 1 starts from this workflow and changes three things. The trigger receives an incident instead of a name, in the shape Sentinel sends, and gains a schema that rejects anything that is not one.
The single HTTP call becomes several, to read the incident's entities and write a comment back, each with a token. And the Response reports what the playbook decided instead of echoing a name. The shape stays the same: a trigger, actions in order, expressions, parameters, and a run history that records it all.
Practice
Run your first workflow your tenantCreate the project, add the workflow, run it, and read its run history, then break it and read that too.
- Create a logic app workspace with a Stateful Workflow named first-run.
- Add HarnessUrl to local.settings.json and the harnessUrl parameter to parameters.json.
- Run the workflow with a request, and compare its reply with the lesson's.
- Break the harness address, run it again, and find the error and the retries in the run history.
Module 1 changes this workflow into the first version of the triage playbook, receiving an incident from Sentinel.