Skip to content

Blocks

A Python step is code, and code is the part of a procedure that the people who have to trust it usually cannot read. Blocks are the other way to write the same step: one line per thing that happens, each line a block with a name and a sentence, and XPressRT turns the file into the Python that runs.

CHECK "the output is stable"
  PURPOSE "Reads the output three times and judges the mean."
  values = READ_SERIES(Unit/U_out, 3)
  average = MEAN(values)
  CHECK_BETWEEN(average, 4.9, 5.1)
END

What comes out is an ordinary Python step, with the same report. The difference to a step written by hand is where it can run: a step built from blocks runs in the browser, with the rest of the run, and needs no local step runner. And who can read it back.

Blocks or Python — which when

This is the question worth answering before anything else, and the answer is per step, not per project. The same procedure can hold both.

Take blocks when the step will be read by somebody who did not write it. A reviewer, an auditor, the colleague who takes over the bench, you in a year. Every block carries a sentence, so the step describes itself: Reads Unit/U_out 3 times, kept as values. Nobody has to read Python to say whether the step does what the procedure claims.

Take blocks when the step has to be approved. Approval means a person read it and stands behind it. That is a different act when what they read is eighteen sentences rather than a page of code — and XPressRT can then point out things that build and run and still do not do what they look like (see below).

Take Python when the step needs something no block offers. A vendor library, a serial protocol, a file format nobody else has, arithmetic that does not fit in the formulas blocks support. Blocks are a closed language on purpose: what cannot be written in them cannot be written in them, and a step that needs more is a Python step, written by hand — or a block of your own workspace (see below).

Take Python when the step is a one-off you will delete again. Finding out whether a rig answers at all is not a document.

What is never a reason either way: can somebody here write Python? If the step is going to be read and approved, blocks are the better form even for a practised programmer. If the step needs a vendor library, Python is the only form even for somebody who would rather not.

An assistant can write both

If an assistant works on this workspace, it can write a block file for you — the language is described in local/blocks/README.md, which XPressRT writes beside the files, and an assistant reads it there. What it cannot do is approve the result. See Working with an assistant.

Where blocks live

local/blocks/ in your workspace, one file per step file: local/blocks/power.blocks becomes local/steps/power.py. The name has to be usable as a Python module — letters, digits and underscores, no dashes.

XPressRT builds the files when the workspace or the step is opened, and before a run. Beside them it writes README.md: the whole language and every block, with the Python each one becomes. That file is generated, so it is always the language this version actually has.

Building also tidies up. A generated file in local/steps/ whose block file is gone is removed, so nothing keeps running that nobody can see any more. An assistant that builds with build_blocks is told which files were removed.

The step itself still has to exist in XPressRT, as a Python step with exactly the sentence and the kind the block file gives it. A block file that holds a sentence XPressRT has no step for is pointed out — Not a step in XPressRT yet — and never runs, because no test case can use it.

Writing a block file

A step starts with its kind and its sentence and ends with END:

SETUP "the supply is set to {voltage} volt"
  PURPOSE "Sets the supply and waits for it to settle."
  WRITE(Supply/U_set, voltage)
  WAIT(2)
END
  • SETUP, ACTION and CHECK are the three kinds of step — the Setup, Action and Check the step window shows. There is no AND or BUT: those are how a test case continues the line above, not what a step is.
  • The sentence is the step's sentence exactly, {braces} and all; every {name} is a value the step can use. A sentence is one step of one kind: if it is a Check in XPressRT and ACTION here, the step does not build, and the same sentence twice in one file does not build either.
  • PURPOSE "…" says what the step is for. It is the same text as the step's description in XPressRT, in one place twice: change it with the step's settings or with describe_step, and both are written. Where they differ — somebody edited this file by hand — the build says so and names what the description reads; it is never overwritten, because that is the text a person approved.
  • One block per line, or several separated by ;. # starts a comment.
  • name = BLOCK(…) keeps what a block gives back. name = average * 2 calculates. name = [] starts a list and ADD_TO(name, …) fills it.
  • FOR name IN [a, b, c] … END repeats, and so does REPEAT 10 TIMES … END.
  • IF value > 7 … ELSE … END decides. Every decision is written into the run log, so the report says afterwards which branch ran.
  • Data points are written as they are named, without quotes: Unit/U_out. Texts are in double quotes, and {name} inside a text puts a value in. A number there is written the way the report writes it: 11.4, not 11.399999999999999, and 5.0 stays 5.0.
  • Times are in seconds where a block waits (WAIT(2), COLLECT, WAIT_UNTIL), and in milliseconds where a block measures time inside a course (TIME_OF, WINDOW, DURATION).
  • Values can be given by position or by name: ATTACH_TABLE(values, caption="series").

What blocks there are

The full list, with every parameter and the Python each block becomes, is in local/blocks/README.md. An overview:

Purpose Blocks
Data points READ reads one value, READ_SERIES several in a row, WRITE sets one.
The operator SHOW shows an instruction and waits for it to be confirmed. CONFIRM asks a yes/no question — no fails the step. ASK_TEXT asks for a text, such as a serial number. Each of them, and READ and READ_SERIES, can show a picture — see Pictures in a dialog.
Waiting and time WAIT waits a number of seconds. WAIT_UNTIL waits until a data point reaches a value and keeps how long that took. COLLECT keeps everything that arrives for a data point within some seconds, with its time stamps. RECORDED takes what this test case already recorded for a point. TIME_OF, TIME_SINCE, WINDOW and DURATION work on such stamped values. CHECK_DENSITY fails when they arrived too far apart. MARK sets a reference — the moment an action took effect, or an event a check found — and since = AT(…) measures from it; see Time has a reference.
What the test case has seen LAST takes the newest value recorded for a data point. CLEAR and CLEAR_ALL start a new window, so what came before stops counting.
Across steps REMEMBER keeps a value for a later step of the same test case, RECALL takes it.
Calculating MEAN, MIN, MAX, SPREAD, PRODUCT, SUBTRACT_FROM_EACH, ROUND, SLOPE, INTERCEPT — and formulas with + - * / and spreadsheet functions such as SQRT or AVERAGE wherever a value stands.
The verdict CHECK_BETWEEN fails the step when a value is outside its limits. CHECK_IS fails it when a value is not the expected one — a name, a text, on/off, or a record against a bundle. CHECK_CONTAINS fails it when a list does not hold a value. FAIL fails it with a sentence of yours. RESULT records a value in the report, judged against limits when you give them — but it does not fail the step.
Evidence NOTE writes a line into the run log. ATTACH_TABLE, ATTACH_CHART and ATTACH_FILE hang a table, a chart of a course or a file on the step. SAVE_EXCEL saves a table as an Excel file. REPORT_DEFECT and ATTACH_PROOF file a defect or a proof report from measured values.

Pictures in a dialog

SHOW, CONFIRM, ASK_TEXT, READ and READ_SERIES take a picture the operator sees in the dialog — what a good seal looks like, where the probe goes:

SHOW("Connect the load to J2.", picture="local/assets/j2.png")
ok = CONFIRM("Does the seal sit flat, as in the picture?", picture="local/assets/seal.png")
U = READ(Unit/U_out, picture="local/assets/probe-point.png")

The picture is a file in local/assets, beside the blocks — put it there the way you put a block file into local/blocks. A picture already uploaded through the step form can be named from uploads as well (picture="uploads/<name>"). Building checks that the file is there: a missing file, or one anywhere else, is an error, because the operator would otherwise be shown a gap where the picture should explain what to do. Without picture a block turns into exactly the Python it always did, so no approved step has to be approved again.

With a test stand answering the reading, READ shows nothing — the picture is for the person.

There is no way to write anything else. A step that needs something no block offers needs a new block: either a block of your own workspace, written and approved as described below, or a Python step written by hand.

On/off and text points

READ of an on/off point gives "on" or "off", and a fixed value is written the same way: WRITE(Bench/Lamp, "on"), CHECK_IS(lamp, "on"), IF lamp = "off". A block compares word for word, so "ON" or "true" does not build there. A text point gives its text; compare it with = or !=.

Checked against the catalogue when the block is built, and a violation does not build:

  • WRITE on a point that is read from the unit;
  • a fixed value that the point cannot hold — WRITE(Bench/Lamp, "maybe"), a number at a text point, a text longer than the point allows;
  • <, >, CHECK_BETWEEN, MEAN or arithmetic on an on/off or text value.

Numbers and named values keep the warnings they had, so a block file that built before builds now.

Requests

A request is called with REQUEST, its parameters given by name, as the catalogue names them:

SETUP "the system is in {state} state"
  antwort = REQUEST(System/BringToState, state = state)
  IF antwort.status != "ok"
    FAIL("Could not bring the unit to {state}: {antwort.message}")
  END
  CHECK_IS(READ(System/State), state)
END

Without a name, anything but ok fails the step. With a name it gives the answer, and the step judges it: antwort.status, antwort.message, antwort.executor, antwort.accepted_at, antwort.completed_at, antwort.clock and antwort.code. A parameter that is not given takes its default when the step runs.

MARK("Trigger") after a REQUEST marks the moment it was accepted, where its effect begins; MARK("Trigger", at = "completed") the moment it ended. A time check then measures from there with since = AT("Trigger"), and AT_POINT(System/BringToState) measures from the newest call.

Checked against the catalogue when the block is built, and a violation does not build: an unknown parameter, a required one left out, a fixed value that does not fit its type, REQUEST on a point that is read or set, and READ, COLLECT, WAIT_UNTIL or WRITE on a request. The step's view names every value the request is called with, the fixed ones and the defaults, so whoever approves the step sees what is called.

Records and lists

A record is several named values that belong together — a message, a report, a bundle of settings. A block reads one as a whole, or one field of it with a colon:

CHECK "the error service reports the pump once"
  reports = COLLECT(ErrorService/Received, 0.5, since = AT("sent"))
  n = COUNT(reports)
  CHECK_BETWEEN(n, 1, 1)
  code = LAST(ErrorService/Received:code)
  CHECK_BETWEEN(code, 4711, 4711)
  severity = LAST(ErrorService/Received:detail:severity)
  CHECK_IS(severity, "high")
  report = LAST(ErrorService/Received)
  CHECK_IS(report, { code = 4711, source = "pump" })
END
  • A field is Point:field, up to three fields deep (ErrorService/Received:detail:severity). READ, LAST, COLLECT, RECORDED and WAIT_UNTIL take it. A field with named values reads as its name, on/off as "on" or "off", a number as a number with the field's unit.
  • A bundle is the value of a record: { code = 4711, source = "pump" }. WRITE sets a record point with one — as a whole, every field that has no default; a single field cannot be set on its own. CHECK_IS with a bundle compares the fields the bundle names, and says the first one that differs: detail:severity is "low", expected "high".
  • A request that takes the fields of a record type is called with them by name, REQUEST(Bus/SendDetail, severity = "high", delay = 20); a parameter that is itself a record takes a bundle, message = { … }.
  • A list is counted with COUNT and searched with CHECK_CONTAINS(values, value) — with a bundle, for an entry whose named fields stand like that. There is no access to one entry by its position.
  • How often a message arrived is COUNT of what COLLECT gathered from a mark: exactly once within 500 ms is the three lines above. A point that reports events can have received nothing — the count is then 0, and the check judges that.

What a record is not is a number: CHECK_BETWEEN, MEAN, a chart or < on a whole record does not build, and says read the field you mean. Checked when the block is built, with the line: a field the type does not have, a colon on a point that holds no record, a field behind a list, a bundle with a field the type does not know or a value that does not fit it, a name that is none of the field's named values. After a record type changed, what no longer fits is exactly such a build error, with its place.

Where the value comes from the step's sentence — REQUEST(Bus/SendDetail, severity = severity) in the detail is sent with severity {severity} — the name stands in the test case, and the block cannot check it when it is built. Check the procedure does: it names the test case and the line that gives the field a value it does not have.

Without a bench nobody sends a message, so somebody states what arrived. The operator gets a dialog for it: a record with one input per field, a list with a row per entry, and for what a point received over time a table with a row per occurrence — the fields as columns and the moment it arrived, counted from the mark where the check names one. No row is an answer for a point that reports events: nothing was received. In a dry run the assistant answers the same questions with an object, a list, or the list of what arrived and when. Either way every field is checked against its type, and what does not fit is refused with the field, what was expected and what came.

A message, end to end

A bus tells a positioning unit what to do with a door, and the unit confirms it. Nothing here is JSON, and nothing needs a bench.

In the catalogue — one record type and two data points:

Record type DoorCommand door: one of open, close · result: one of isOkay, isError
Request Bus/SendDoorCommand takes the fields of DoorCommand; instruction Send door {door} with result {result} with the bus tool.
Read point Positioning/Confirmed holds a DoorCommand, kind Event

Two steps:

ACTION "the bus sends door {door} with result {result} to Positioning"
  PURPOSE "Sends a door command on the bus and marks the moment it went out."
  REQUEST(Bus/SendDoorCommand, door = door, result = result)
  MARK("sent")
END

CHECK "Positioning confirms door {door} exactly once within 500 ms"
  PURPOSE "Counts the confirmations within 500 ms of the command and checks which door was confirmed."
  confirmations = COLLECT(Positioning/Confirmed, 0.5, since = AT("sent"))
  n = COUNT(confirmations)
  CHECK_BETWEEN(n, 1, 1)
  which = LAST(Positioning/Confirmed:door)
  CHECK_IS(which, door)
END

The test case names values, one row per message:

When the bus sends door <door> with result <result> to Positioning
Then Positioning confirms door <door> exactly once within 500 ms

| door  | result  |
| close | isOkay  |
| open  | isOkay  |

In a run without a bench the operator gets the request with its two values as a table and the instruction with them filled in, and answers Done. Then the table of what arrived: one row per confirmation — the door, the result, and how many milliseconds after the command. One row at 120 ms passes; two rows fail exactly once; a row that confirms the other door fails on the field. The report shows the request with its values and the confirmations as a table, each with its moment.

A new message is a new row. open with isError did not occur above: add the row and run. The catalogue does not change — the fields and their values are already described. With a point that held whole messages as named values, every new message was a new name in the catalogue.

How big a step is

A step is one action and its verdict — what somebody wants to see pass or fail on its own. Keep an acquisition and its evaluation together where you can, however many blocks that takes.

Where they cannot be together, say so: REMEMBER("curve", curve) in one step and curve = RECALL("curve") in a later one. It holds within a test case and is empty in the next. Both lines appear in the step's description, so a reader sees that this step depends on an earlier one.

Time has a reference

"Within 200 ms the device reports fault 12" says 200 ms after something. A time check names that something: it measures since = AT(…), from a mark a step set with MARK.

ACTION "the operator presses the start key"
  WRITE(Bench/Start, 1)
  MARK("Trigger")
END

CHECK "within 200 ms the device reports fault 12"
  t = WAIT_UNTIL(Unit/Fault, "=", 12, 0.2, since = AT("Trigger"))
  RESULT("until the fault", t, "ms", 0, 200)
END
  • MARK("name") marks a moment. In an action it is the moment its last WRITE took effect; in a check, the moment of the event the check found — so "answer B comes 30 ms after answer A" is a check that finds A and marks it, and a check after it that measures B since = AT("A"). A step with nothing to mark — no WRITE, no event found — fails and says so: a reference is the moment of an effect, not the moment of a line.
  • A set or read step from the form marks itself, in every run, without a field to fill in: under its line as the test case writes it (the user opens the service door) and under its sentence with braces (the user opens the {door}), with the stamp of the last value it set or read. AT("the user opens the {door}") is its newest occurrence in the test case, whatever the value. In AT, a {name} that is a value of the checking step is filled in — then it names that one line —, any other stays as written.
  • AT("name") is that mark; AT() without a name is the newest MARK of this test case — not a form step's own mark, which would otherwise be the last setup step that set anything. Marks belong to their test case: the next one starts without any. A check with AT("x") where no step marked x before it fails with No reference "x" was marked before this check — it does not quietly measure from somewhere else.
  • since goes on WAIT_UNTIL, COLLECT, TIME_OF, WINDOW, LAST and ATTACH_CHART, and TIME_SINCE takes AT(…) as its reference. With it, the time is counted from the mark, and what was recorded since the mark counts too: a check after a 2-second window still measures its 200 ms from the trigger, not from second 2. Without a bench, a course a person or the assistant states for such a check counts from the mark as well — for numbers and for records alike.
  • AT_POINT(Folder/Name) is the moment of the newest value of a point in this test case — set, read or measured, from a block or a form step — for a check that knows its trigger's point: since = AT_POINT(Bench/FootPedal/left). Without a value of that point the check fails and says so.
  • A mark on an observed event counts from after it. A check that marks the request it found, followed by "the same request again within 5000 ms", finds the next request, not the marked one at 0 ms.
  • An event is a change after the mark. A fault that was already there before the trigger is not "reported within 200 ms" — the check fails with It was already = 12 before the reference, also when the bench only reports it again after the mark. Whether it stood is decided by the state at the moment of the mark: the last value before it, which the bench hands over with the course. Where nobody knows that value, the first matching value after the mark is taken as the change, and the result says state at the reference unknown — the first value after it is taken as the change. A data point that reports events — a request on a bus, a telegram — is different: every report after the mark counts, also one that says the same as the last. Its kind is set in the catalogue; see Data points.
  • The time comes from the bench: the stamp of the moment the effect took place or the event happened, with the clock that stamped it. Without a bench, the stamp is what there is — the moment the operator confirmed (entered), the moment the runner sent it (runner), or simulated in a dry run an assistant answers — and the record says so; see Times and what they are measured from.
  • ATTACH_CHART(…, since = AT("Trigger")) puts 0 at the mark and draws it as a line with its name — only the picture; it judges nothing.

Without since a WAIT_UNTIL measures from the start of its own step, COLLECT from the moment it is called and TIME_OF from the first value of its window — after whatever the checks before them took. A check like that builds only if it is unchanged since the last build: a new or changed one does not build (see below), an existing one keeps running with the warning needs a reference, and its result in the report says measured from the start of this step — no reference. Renaming a value of a data point does not count as a change.

A check that names its trigger

Which step is the trigger is a question for the requirement: the event it says causes the reaction. A check written for one test case can find its mark by name. A check that serves many test cases names its trigger in its sentence instead, as a placeholder, and each test case writes it out:

CHECK "the interlock goes LOW within {ms} ms after {trigger}"
  t = WAIT_UNTIL(Bench/Interlock, "=", 0, ms / 1000, since = AT(trigger))
  RESULT("until LOW", t, "ms", 0, ms)
END
When the operator removes the cover
Then the interlock goes LOW within 200 ms after the operator removes the cover
  • Every step marks itself under its line when it is done: a form step, a block step, a Python step, a question to the operator. It marks with the moment of its last effect: the last value it set or read, the event it found, or the moment the operator answered. So AT(trigger) finds the line as the test case wrote it, wherever it stands before the check: in the steps, the background or the preparation. A step that leaves no moment, such as a wait, marks nothing. A step that sets and reads marks with what it set; a step that only reads — a form read, a READ or LAST in a block — marks with the value it read, and the check counts strictly from after it.
  • The last occurrence before the check counts. To pick a line that runs more than once, add its number: … after the operator removes the cover (1).
  • XPressRT checks the trigger whenever a test case is written or saved, and it only points out problems, it never refuses. It flags a trigger that is no line before the check (“…” is no line before this check in this test case. The lines before it: …), a line that comes only after the check, and a line that leaves no moment to measure from. The run still starts, and a wrong trigger fails there with its sentence.
  • In the editor, such a check shows ⏱ beside its line. The tooltip names the step it measures from, and the ⏱ turns orange with what is wrong. As you type the placeholder, the lines before it are offered.
  • Renaming a line carries its triggers along. A check that named the old line names the new one, (n) included.

XPressRT makes sure there is a reference and that it comes from the bench; it does not guess which step it should be.

The three views of a step

A step built from blocks opens in the step window with three views, and the difference between them matters:

View What it shows
Text The step as a flow diagram: a box per thing that happens, arrows between them, a frame around what is repeated, and two lanes side by side where the step decides. No block and no code in the way — this is the view a step opens in, and the one to read a step back in.
Text + blocks The lines as they stand in the file, each with its sentence under it, and which file they come from.
Python The code that was built from them.

Above the views:

  • English and Deutsch switch the language of the sentences. Your browser remembers the choice.
  • Example value fields appear for every {name} in the step's sentence. Fill one in and the sentences show the numbers worked out instead of the formula — 4.9 rather than voltage − 0.1.
  • Enlarge shows the step in a window of its own, large enough to go through with somebody. Shrink puts it back.

The Python view says where its contents came from: Written by XPressRT from local/blocks/power.blocks — it is rewritten every time the blocks are built, so edits here do not last. Change the blocks instead. That sentence is the whole point of the view: this is the place to look at the code, not the place to change it.

In the browser, what runs is the Python XPressRT has just built from the block files, not whatever lies in local/steps/. A file there whose first line claims Generated by XPressRT but that was not built from a block file does not run in the browser.

A step written in Python by hand opens in the same window with the same three tabs, but in the Python view. It says Written by hand, and nothing here rewrites it. Text and Text + blocks say This step is not built from blocks, because there are no sentences to show.

What XPressRT points out, and why

Some things build and run and still do not do what they look like. XPressRT names them instead of letting a run hide them — they are warnings, not refusals:

  • A check with no verdict in it. A CHECK step in which nothing can fail reports "passed" for every unit that ever goes through it.
  • Limits in the report, but nothing that fails. The numbers are in the record, and the step passes whatever they are.
  • Reading immediately after setting. The reading is from before the change as often as not.
  • A value that is fetched and never used. Either the evaluation is missing or the acquisition is.
  • A loop that does not use its variable. It does the same thing ten times.
  • A value used as something it is not — a list where a number belongs.
  • Time computed from values that carry no stamps. The answer is arithmetic on nothing.
  • A time check that names no reference. A WAIT_UNTIL, COLLECT or TIME_OF in a check without since measures from the start of its step — see Time has a reference. This is the one warning that becomes a refusal: for a new or changed check.
  • A time span across two clocks, or not on a bench clock. The mark takes its stamp from one point and the check measures another, and the catalogue gives them different clocks — or the marked point has no clock, so without a bench its stamp is the operator's confirmation.

A warning is not a rule against you. A check that decides with FAIL inside an IF is right, and is pointed out anyway when nothing else in the step can fail. And what is never judged here is whether a limit is correct — 4.9 to 5.1 instead of 4.5 to 5.5 is your call. These say whether a verdict is being reached at all.

What does not build is different: a data point the workspace does not have, a RECALL of something no step remembers, a sentence under the wrong kind, a new or changed time check without since. The step window lists those with their line numbers, and until they are fixed the step fails with the first of them.

What only a test case can show is checked with the procedure, when your assistant checks it with check_procedure: an AT("x") in a test case where no step before it marks x, in the order the steps run.

Trying and running a block step

A step built from blocks runs in the browser. You do not have to start anything:

  • Test Step in the step window works without a runner. The step asks for what it reads, exactly as it would in a run.
  • The Runner not running badge and How to start it do not appear for a block step: nothing is missing. The badge only appears once a runner is connected, and then says Runner connected or Runner · debugging.

Start the local step runner only when you want it: to stop a block step at a breakpoint, or because a bench is wired to this machine. While it is running, it takes over and runs the block steps too. A runner.json left behind by a runner that has stopped does not count — if nothing answers, the step runs in the browser.

Two things hold in the browser just as they do with a runner:

  • The step's timeout. Timeout (seconds) counts the step's own work, not the time its dialogs are open; a dialog waits up to five minutes for an answer and is then withdrawn. The values somebody typed before that stay in the record. See Time limits.
  • Stopping. Stop the run while a question is open ends the run, and still writes a report of what got through up to then.

Approving a step

A step built from blocks is read by a person and approved by them. For that approval to be worth anything it has to refer to something fixed, so XPressRT records two fingerprints:

  • the blocks, as they stood. Change a line and the approval is no longer the approval of this step.
  • the Python they became. This is the one you lose without meaning to: the block file is untouched, the sentences are untouched, and a new version of XPressRT builds a block differently — so what runs is something nobody approved. The version of every block used is recorded beside it.

A step whose approval no longer holds says which of the two moved, and who had approved it. Approving again is a person's act, in the step window.

A rename is not a change. Rename a data point, a folder of data points or the name of a state, and an approved step that names it stays approved: it does the same, under another name. Who approved and when stay as they were; the history entry of the rename says for every such step approval carried over the rename, with the approver and the date — carried, not given anew. A step that had already changed stays changed, and a rename that also changes the point (its unit, its type, its values) is a change.

An approval can also be withdrawn in the step window — with your name and a reason, both required. It is not deleted: it stays in the record with who withdrew it, when and why, and the step counts as not approved until somebody approves it again. The step window shows Not approved in its header for such a step, and the step list filters block steps by Approved and Not approved — a step changed since its approval counts as not approved. A Python step without blocks has no approval and is not in that filter.

An approval is not a signature

The record lives in your workspace (local/blocks/approvals.json), and whoever can write there can write into it. It says that something was approved and what that referred to — not that nobody lied. If you need more than that, that is a question about identity, and one to settle before an audit rather than during one.

Blocks of your own workspace

A workspace can bring blocks of its own, for the thing only your bench has. A developer describes each one in local/blocks/extensions.json — its name, its sentence in English and German, its parameters, the function it calls and a version — and writes that function in a module under local/extensions/.

Such a block carries its own approval: a block that generates Python is code, and code that arrived with a folder should not run because it was in the folder. Settings › Blocks lists them and is where they are approved, one by one, by name. Until then a step that uses one does not build, and says why.

A block whose definition or generated Python changed since it was approved stops being available and says so. That is the same rule as for steps, for the same reason.

What this does not change

  • The report. RESULT, the attachments and the rest end up in the record under the step that produced them, the same way as from a step written by hand.
  • Test cases. A test case uses the sentence. It cannot tell, and does not need to, whether the step behind it was written in blocks or by hand.
  • Data points. A block step names data points like any other step, and is counted among the steps that use one. A block step whose sentence has no step in XPressRT any more never runs. When an assistant deletes a data point, such a step does not hold that up, but it is named: its block file still refers to the point.