Skip to content

feat: Allow actions to return result values, exposed to the subsequent sequential action as $(this:result)#4065

Open
jswalden wants to merge 1 commit intobitfocus:mainfrom
jswalden:action-callback-return-result
Open

feat: Allow actions to return result values, exposed to the subsequent sequential action as $(this:result)#4065
jswalden wants to merge 1 commit intobitfocus:mainfrom
jswalden:action-callback-return-result

Conversation

@jswalden
Copy link
Copy Markdown
Contributor

@jswalden jswalden commented Apr 2, 2026

The one moderately-major thing in all this that I am uncertain about, is how to propagate a result to a subsequent concurrent action group that contains waits.

If I read the code correctly, Companion basically splits concurrent action groups into sets of non-wait actions, and then it performs all those non-waits of the first set at once, then does the intervening wait, then does the next set of non-waits, etc.

I think it is reasonable to propagate a previous result to all actions in a concurrent action group that doesn't contain any waits. It is less clear that it makes sense to do so when the action group contains waits. Or that they should propagate beyond the first set of actions? For prototype-ful purposes I just acted like the waits weren't there. For actual suitability for landing purposes, maybe that should change. Don't propagate if there are any waits? Only propagate to actions before the first wait? The answer isn't immediately obvious to me.

Summary by CodeRabbit

  • New Features

    • Actions executed in sequence can now share results through the new $(this:result) variable, allowing subsequent actions to dynamically access and use the output of the preceding action
  • Documentation

    • Added documentation for the new $(this:result) builtin local variable, providing access to the result returned by the previously executed action in an action sequence

…entially succeeding actions as `$(this:result)`.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 2, 2026

📝 Walkthrough

Walkthrough

This pull request introduces action result chaining, allowing actions executed sequentially to access the previous action's result via a new $(this:result) variable. Return types are updated across the action execution pipeline to propagate VariableValue results, with threading through parser creation methods and IPC message contracts.

Changes

Cohort / File(s) Summary
Action Execution Core
companion/lib/Controls/ActionRunner.ts, companion/lib/Controls/ControlStore.ts, companion/lib/Controls/Controller.ts, companion/lib/Controls/IControlStore.ts
runMultipleActions and runActions now return Promise<VariableValue> instead of Promise<void>, sequentially threading action results to subsequent actions. createVariablesAndExpressionParser methods accept new previousResult parameter.
Action Handler & IPC Layer
companion/lib/Instance/Connection/ChildHandlerApi.ts, ChildHandlerLegacy.ts, ChildHandlerNew.ts, IpcTypesNew.ts, Thread/Entrypoint.ts
actionRun methods now return Promise<VariableValue>. RunActionExtras includes new previousResult field. ExecuteActionResponseMessage refactored into discriminated union with success result included.
Control Type Updates
companion/lib/Controls/ControlTypes/Button/Base.ts, Button/Preset.ts, Button/Util.ts, Triggers/Trigger.ts
Parser invocations updated to pass undefined (or mapped value) for new previousResult parameter in expression creation calls.
Entity & Variable System
companion/lib/Controls/Entities/EntityIsInvertedManager.ts, EntityListPoolBase.ts, EntityManager.ts, companion/lib/Variables/Values.ts
CreateVariablesAndExpressionParser type and implementations updated to accept previousResult. VariablesValues now injects $(this:result) with the action result value.
Miscellaneous Controllers
companion/lib/Internal/Controller.ts, companion/lib/ImportExport/Backups.ts, ImportExport/Export.ts, companion/lib/Preview/ExpressionStream.ts, Preview/Graphics.ts, companion/lib/Surface/Controller.ts
Parser creation calls updated to include previousResult parameter (typically undefined or mapped from context).
UI & Documentation
webui/src/Controls/LocalVariablesStore.tsx, docs/user-guide/3_config/variables.md
Added this:result local variable option with description "the result of the action sequentially preceding this one." Documentation updated to reflect new variable.
Dependencies
companion/package.json
@companion-module/host changed from 1.0.2 to local file reference.

Poem

🎬 Actions now remember what came before,
Results flowing onward to unlock new doors,
$(this:result) holds the chain so tight,
Sequential magic, each step shining bright! ✨

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main feature: allowing actions to return result values accessible to subsequent sequential actions via the new $(this:result) variable.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
companion/lib/Internal/Controller.ts (1)

448-470: ⚠️ Potential issue | 🟠 Major

Internal action results are still dropped on this path.

This now wires $(this:result) into parsing, but Line 448 still returns Promise<void>, and the fragment dispatch below still treats the fragment return as a truthy “handled” flag. The result is that internal actions cannot propagate any value to the next action, and falsy results like 0, false, or '' additionally fall through as unhandled. Please switch this path to an explicit handled/result contract and return the actual result from executeAction().

Also applies to: 496-505

companion/lib/Controls/ActionRunner.ts (1)

89-110: ⚠️ Potential issue | 🟠 Major

Please lock down previousResult semantics across concurrent groups with waits

Friendly heads-up: actions after a wait currently still receive the same extras.previousResult as actions before the wait (Line 104), which matches the prototype but leaves ambiguous behavior in a user-visible path. This can make $(this:result) feel non-deterministic once waits are involved.

I’d recommend choosing and enforcing one explicit policy here (e.g., clear after first wait, or disable propagation when any wait exists).

Suggested direction (disable propagation when waits are present)
 		} else {
 			const groupedActions = this.#splitActionsAroundWaits(actions)
+			const hasWaits = groupedActions.some((group) => !!group.waitAction)

 			const ps: Promise<VariableValue>[] = []

 			for (const { waitAction, actions } of groupedActions) {
 				if (extras.abortDelayed.aborted) break
@@
 				// Spawn all the actions in parallel
 				for (const action of actions) {
+					const actionExtras: RunActionExtras = {
+						...extras,
+						previousResult: hasWaits ? undefined : extras.previousResult,
+					}
 					ps.push(
-						this.#runAction(action, extras).catch((e) => {
+						this.#runAction(action, actionExtras).catch((e) => {
 							this.#logger.silly(`Error executing action for ${action.connectionId}: ${e.message ?? e}`)
 							return undefined
 						})
 					)
 				}
🧹 Nitpick comments (2)
companion/lib/Controls/IControlStore.ts (1)

48-52: Consider making previousResult optional in the interface

Nice addition overall. For this interface, making previousResult optional can keep behavior the same while avoiding repetitive undefined at every non-action call site.

Proposed tweak
 createVariablesAndExpressionParser(
 	controlId: string | null | undefined,
 	overrideVariableValues: VariableValues | null,
-	previousResult: VariableValue
+	previousResult?: VariableValue
 ): VariablesAndExpressionParser
companion/lib/Controls/ActionRunner.ts (1)

75-83: Nice chaining logic — consider avoiding in-place mutation of extras

This works, but mutating extras.previousResult directly (Line 77) makes the function stateful against its input object. Using a local previousResult (and passing a fresh extras object per action) keeps behavior easier to reason about.

Refactor sketch
 		if (executeSequential) {
 			// Future: abort on error?
+			let previousResult = extras.previousResult

 			for (const action of actions) {
 				if (extras.abortDelayed.aborted) break
-				extras.previousResult = await this.#runAction(action, extras).catch((e) => {
+				previousResult = await this.#runAction(action, { ...extras, previousResult }).catch((e) => {
 					this.#logger.silly(`Error executing action for ${action.connectionId}: ${e.message ?? e}`)
 					return undefined
 				})
 			}

-			return extras.previousResult
+			return previousResult

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7d3bd3af-aba0-4f33-b03d-b6cd4ddcdc72

📥 Commits

Reviewing files that changed from the base of the PR and between 0a7ada7 and 21b0e38.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (26)
  • companion/lib/Controls/ActionRunner.ts
  • companion/lib/Controls/ControlStore.ts
  • companion/lib/Controls/ControlTypes/Button/Base.ts
  • companion/lib/Controls/ControlTypes/Button/Preset.ts
  • companion/lib/Controls/ControlTypes/Button/Util.ts
  • companion/lib/Controls/ControlTypes/Triggers/Trigger.ts
  • companion/lib/Controls/Controller.ts
  • companion/lib/Controls/Entities/EntityIsInvertedManager.ts
  • companion/lib/Controls/Entities/EntityListPoolBase.ts
  • companion/lib/Controls/IControlStore.ts
  • companion/lib/ImportExport/Backups.ts
  • companion/lib/ImportExport/Export.ts
  • companion/lib/Instance/Connection/ChildHandlerApi.ts
  • companion/lib/Instance/Connection/ChildHandlerLegacy.ts
  • companion/lib/Instance/Connection/ChildHandlerNew.ts
  • companion/lib/Instance/Connection/EntityManager.ts
  • companion/lib/Instance/Connection/IpcTypesNew.ts
  • companion/lib/Instance/Connection/Thread/Entrypoint.ts
  • companion/lib/Internal/Controller.ts
  • companion/lib/Preview/ExpressionStream.ts
  • companion/lib/Preview/Graphics.ts
  • companion/lib/Surface/Controller.ts
  • companion/lib/Variables/Values.ts
  • companion/package.json
  • docs/user-guide/3_config/variables.md
  • webui/src/Controls/LocalVariablesStore.tsx

Comment on lines +124 to +125
- The result of the action sequentially preceding this one
- Variable: `this:result`
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Please add a short caveat for concurrent/wait action groups

Love that this:result is documented. Given current implementation notes, a brief line clarifying that behavior is guaranteed for sequential chains (and may differ for concurrent groups with waits) would help avoid user confusion.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Brevity/clarity versus precision tradeoff. Can tweak if someone wants it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jswalden — totally fair point! Here's a minimal phrasing that tries to stay concise while flagging the edge case:

- The result of the action sequentially preceding this one (sequential action chains only)
  - Variable: `this:result`

Or alternatively, a single parenthetical note tucked on the same line keeps the list clean without adding extra bullet depth. Either way, happy to leave it as-is if you'd prefer to revisit once the concurrent-group propagation semantics are finalized — the current wording isn't wrong, just silent on the edge case.

@jswalden
Copy link
Copy Markdown
Contributor Author

jswalden commented Apr 2, 2026

Internal action results are still dropped on this path.

I chose for internal actions to not produce their own results, and also not to propagate a prior result across them. (Why should they? Don't see good reason for them to, honestly.) It makes most sense IMO for only an action that produces a result itself, to be source of one.

Please lock down previousResult semantics across concurrent groups with waits

Flagged in the initial comment.

Consider making previousResult optional in the interface

Explicit is better than implicit. There's always a previous result value/variable. It's not actually optional semantically. (I even considered using const previousResult: VariableValue = undefined every place, for documentation, but didn't carry through on it.)

Nice chaining logic — consider avoiding in-place mutation of extras

I kind of prefer less object creation churn, but Julian's the one who will have a meaningful preference here, probably.

@jswalden
Copy link
Copy Markdown
Contributor Author

jswalden commented Apr 2, 2026

This will fix #4064

@jswalden jswalden changed the title feat: Allow actions to return result values, exposed to the subsequent sequential action as $(this:result). (#4064) feat: Allow actions to return result values, exposed to the subsequent sequential action as $(this:result) Apr 2, 2026
@makstech
Copy link
Copy Markdown
Contributor

makstech commented Apr 2, 2026

The idea is awesome and I 100% support it but my suggestion to your question

how to propagate a result to a subsequent concurrent action group that contains waits.

Why not use something like n8n does it: any node that's downstream, can access any upstream node's result using the node's name. For example, first node is called "Message", the next node can get the data directly with $json (in n8n) or $(this:result) with your implementation in Companion, but then next nodes or even the current one, can also get it with $('Message').item.json. This is an example that works elsewhere and is just an idea, but obviously completely out of scope for this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants