griptape-nodes 0.52.0__py3-none-any.whl → 0.53.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. griptape_nodes/__init__.py +6 -943
  2. griptape_nodes/__main__.py +6 -0
  3. griptape_nodes/app/api.py +1 -12
  4. griptape_nodes/app/app.py +256 -209
  5. griptape_nodes/cli/__init__.py +1 -0
  6. griptape_nodes/cli/commands/__init__.py +1 -0
  7. griptape_nodes/cli/commands/config.py +71 -0
  8. griptape_nodes/cli/commands/engine.py +80 -0
  9. griptape_nodes/cli/commands/init.py +548 -0
  10. griptape_nodes/cli/commands/libraries.py +90 -0
  11. griptape_nodes/cli/commands/self.py +117 -0
  12. griptape_nodes/cli/main.py +46 -0
  13. griptape_nodes/cli/shared.py +84 -0
  14. griptape_nodes/common/__init__.py +1 -0
  15. griptape_nodes/common/directed_graph.py +55 -0
  16. griptape_nodes/drivers/storage/local_storage_driver.py +7 -2
  17. griptape_nodes/exe_types/core_types.py +60 -2
  18. griptape_nodes/exe_types/node_types.py +38 -24
  19. griptape_nodes/machines/control_flow.py +86 -22
  20. griptape_nodes/machines/fsm.py +10 -1
  21. griptape_nodes/machines/parallel_resolution.py +570 -0
  22. griptape_nodes/machines/{node_resolution.py → sequential_resolution.py} +22 -51
  23. griptape_nodes/mcp_server/server.py +1 -1
  24. griptape_nodes/retained_mode/events/base_events.py +2 -2
  25. griptape_nodes/retained_mode/events/node_events.py +4 -3
  26. griptape_nodes/retained_mode/griptape_nodes.py +25 -12
  27. griptape_nodes/retained_mode/managers/agent_manager.py +9 -5
  28. griptape_nodes/retained_mode/managers/arbitrary_code_exec_manager.py +3 -1
  29. griptape_nodes/retained_mode/managers/context_manager.py +6 -5
  30. griptape_nodes/retained_mode/managers/flow_manager.py +117 -204
  31. griptape_nodes/retained_mode/managers/library_lifecycle/library_directory.py +1 -1
  32. griptape_nodes/retained_mode/managers/library_manager.py +35 -25
  33. griptape_nodes/retained_mode/managers/node_manager.py +81 -199
  34. griptape_nodes/retained_mode/managers/object_manager.py +11 -5
  35. griptape_nodes/retained_mode/managers/os_manager.py +24 -9
  36. griptape_nodes/retained_mode/managers/secrets_manager.py +8 -4
  37. griptape_nodes/retained_mode/managers/settings.py +32 -1
  38. griptape_nodes/retained_mode/managers/static_files_manager.py +8 -3
  39. griptape_nodes/retained_mode/managers/sync_manager.py +8 -5
  40. griptape_nodes/retained_mode/managers/workflow_manager.py +110 -122
  41. griptape_nodes/traits/add_param_button.py +1 -1
  42. griptape_nodes/traits/button.py +216 -6
  43. griptape_nodes/traits/color_picker.py +66 -0
  44. griptape_nodes/traits/traits.json +4 -0
  45. {griptape_nodes-0.52.0.dist-info → griptape_nodes-0.53.0.dist-info}/METADATA +2 -1
  46. {griptape_nodes-0.52.0.dist-info → griptape_nodes-0.53.0.dist-info}/RECORD +48 -34
  47. {griptape_nodes-0.52.0.dist-info → griptape_nodes-0.53.0.dist-info}/WHEEL +0 -0
  48. {griptape_nodes-0.52.0.dist-info → griptape_nodes-0.53.0.dist-info}/entry_points.txt +0 -0
@@ -42,6 +42,14 @@ class ResolutionContext:
42
42
  self.focus_stack = []
43
43
  self.paused = False
44
44
 
45
+ @property
46
+ def current_node(self) -> BaseNode:
47
+ """Get the currently focused node from the focus stack."""
48
+ if not self.focus_stack:
49
+ msg = "No node is currently in focus - focus stack is empty"
50
+ raise RuntimeError(msg)
51
+ return self.focus_stack[-1].node
52
+
45
53
  def reset(self) -> None:
46
54
  if self.focus_stack:
47
55
  node = self.focus_stack[-1].node
@@ -55,7 +63,7 @@ class InitializeSpotlightState(State):
55
63
  @staticmethod
56
64
  async def on_enter(context: ResolutionContext) -> type[State] | None:
57
65
  # If the focus stack is empty
58
- current_node = context.focus_stack[-1].node
66
+ current_node = context.current_node
59
67
  GriptapeNodes.EventManager().put_event(
60
68
  ExecutionGriptapeNodeEvent(
61
69
  wrapped_event=ExecutionEvent(payload=CurrentDataNodeEvent(node_name=current_node.name))
@@ -70,7 +78,7 @@ class InitializeSpotlightState(State):
70
78
  # If the focus stack is empty
71
79
  if not len(context.focus_stack):
72
80
  return CompleteState
73
- current_node = context.focus_stack[-1].node
81
+ current_node = context.current_node
74
82
  if current_node.state == NodeResolutionState.UNRESOLVED:
75
83
  # Mark all future nodes unresolved.
76
84
  # TODO: https://github.com/griptape-ai/griptape-nodes/issues/862
@@ -95,7 +103,7 @@ class InitializeSpotlightState(State):
95
103
  class EvaluateParameterState(State):
96
104
  @staticmethod
97
105
  async def on_enter(context: ResolutionContext) -> type[State] | None:
98
- current_node = context.focus_stack[-1].node
106
+ current_node = context.current_node
99
107
  current_parameter = current_node.get_current_parameter()
100
108
  if current_parameter is None:
101
109
  return ExecuteNodeState
@@ -116,7 +124,7 @@ class EvaluateParameterState(State):
116
124
 
117
125
  @staticmethod
118
126
  async def on_update(context: ResolutionContext) -> type[State] | None:
119
- current_node = context.focus_stack[-1].node
127
+ current_node = context.current_node
120
128
  current_parameter = current_node.get_current_parameter()
121
129
  from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
122
130
 
@@ -143,48 +151,6 @@ class EvaluateParameterState(State):
143
151
 
144
152
 
145
153
  class ExecuteNodeState(State):
146
- # TODO: https://github.com/griptape-ai/griptape-nodes/issues/864
147
- @staticmethod
148
- async def clear_parameter_output_values(context: ResolutionContext) -> None:
149
- """Clears all parameter output values for the currently focused node in the resolution context.
150
-
151
- This method iterates through each parameter output value stored in the current node,
152
- removes it from the node's parameter_output_values dictionary, and publishes an event
153
- to notify the system about the parameter value being set to None.
154
-
155
- Args:
156
- context (ResolutionContext): The resolution context containing the focus stack
157
- with the current node being processed.
158
-
159
- Raises:
160
- ValueError: If a parameter name in parameter_output_values doesn't correspond
161
- to an actual parameter in the node.
162
-
163
- Note:
164
- - Uses a copy of parameter_output_values to safely modify the dictionary during iteration
165
- - For each parameter, publishes a ParameterValueUpdateEvent with value=None
166
- - Events are wrapped in ExecutionGriptapeNodeEvent before publishing
167
- """
168
- current_node = context.focus_stack[-1].node
169
- for parameter_name in current_node.parameter_output_values.copy():
170
- parameter = current_node.get_parameter_by_name(parameter_name)
171
- if parameter is None:
172
- err = f"Attempted to execute node '{current_node.name}' but could not find parameter '{parameter_name}' that was indicated as having a value."
173
- raise ValueError(err)
174
- parameter_type = parameter.type
175
- if parameter_type is None:
176
- parameter_type = ParameterTypeBuiltin.NONE.value
177
- payload = ParameterValueUpdateEvent(
178
- node_name=current_node.name,
179
- parameter_name=parameter_name,
180
- data_type=parameter_type,
181
- value=None,
182
- )
183
- GriptapeNodes.EventManager().put_event(
184
- ExecutionGriptapeNodeEvent(wrapped_event=ExecutionEvent(payload=payload))
185
- )
186
- current_node.parameter_output_values.clear()
187
-
188
154
  @staticmethod
189
155
  async def collect_values_from_upstream_nodes(context: ResolutionContext) -> None:
190
156
  """Collect output values from resolved upstream nodes and pass them to the current node.
@@ -199,7 +165,7 @@ class ExecuteNodeState(State):
199
165
  """
200
166
  from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
201
167
 
202
- current_node = context.focus_stack[-1].node
168
+ current_node = context.current_node
203
169
  connections = GriptapeNodes.FlowManager().get_connections()
204
170
 
205
171
  for parameter in current_node.parameters:
@@ -237,14 +203,18 @@ class ExecuteNodeState(State):
237
203
 
238
204
  @staticmethod
239
205
  async def on_enter(context: ResolutionContext) -> type[State] | None:
240
- current_node = context.focus_stack[-1].node
206
+ current_node = context.current_node
241
207
 
242
208
  # Clear all of the current output values
243
209
  # if node is locked, don't clear anything. skip all of this.
244
210
  if current_node.lock:
245
211
  return ExecuteNodeState
246
212
  await ExecuteNodeState.collect_values_from_upstream_nodes(context)
247
- await ExecuteNodeState.clear_parameter_output_values(context)
213
+
214
+ # Clear all of the current output values but don't broadcast the clearing.
215
+ # to avoid any flickering in subscribers (UI).
216
+ context.current_node.parameter_output_values.silent_clear()
217
+
248
218
  for parameter in current_node.parameters:
249
219
  if ParameterTypeBuiltin.CONTROL_TYPE.value.lower() == parameter.output_type:
250
220
  continue
@@ -398,7 +368,7 @@ class CompleteState(State):
398
368
  return None
399
369
 
400
370
 
401
- class NodeResolutionMachine(FSM[ResolutionContext]):
371
+ class SequentialResolutionMachine(FSM[ResolutionContext]):
402
372
  """State machine for resolving node dependencies."""
403
373
 
404
374
  def __init__(self) -> None:
@@ -418,6 +388,7 @@ class NodeResolutionMachine(FSM[ResolutionContext]):
418
388
  def is_started(self) -> bool:
419
389
  return self._current_state is not None
420
390
 
421
- def reset_machine(self) -> None:
391
+ # Unused argument but necessary for parallel_resolution because of futures ending during cancel but not reset.
392
+ def reset_machine(self, *, cancel: bool = False) -> None: # noqa: ARG002
422
393
  self._context.reset()
423
394
  self._current_state = None
@@ -69,7 +69,7 @@ mcp_server_logger.addHandler(RichHandler(show_time=True, show_path=False, markup
69
69
  mcp_server_logger.setLevel(logging.INFO)
70
70
 
71
71
 
72
- def main_sync(api_key: str) -> None:
72
+ def start_mcp_server(api_key: str) -> None:
73
73
  """Synchronous version of main entry point for the Griptape Nodes MCP server."""
74
74
  mcp_server_logger.debug("Starting MCP GTN server...")
75
75
  # Give these a session ID
@@ -135,7 +135,7 @@ class SkipTheLineMixin:
135
135
  class ResultPayloadSuccess(ResultPayload, ABC):
136
136
  """Abstract base class for success result payloads."""
137
137
 
138
- result_details: ResultDetails | str = "Success"
138
+ result_details: ResultDetails | str
139
139
 
140
140
  def __post_init__(self) -> None:
141
141
  """Initialize success result with INFO level default for strings."""
@@ -156,7 +156,7 @@ class ResultPayloadSuccess(ResultPayload, ABC):
156
156
  class ResultPayloadFailure(ResultPayload, ABC):
157
157
  """Abstract base class for failure result payloads."""
158
158
 
159
- result_details: ResultDetails | str = "Failure"
159
+ result_details: ResultDetails | str
160
160
  exception: Exception | None = None
161
161
 
162
162
  def __post_init__(self) -> None:
@@ -3,6 +3,7 @@ from enum import Enum, auto
3
3
  from typing import Any, NamedTuple, NewType
4
4
  from uuid import uuid4
5
5
 
6
+ from griptape_nodes.exe_types.core_types import NodeMessagePayload
6
7
  from griptape_nodes.exe_types.node_types import NodeResolutionState
7
8
  from griptape_nodes.node_library.library_registry import LibraryNameAndVersion
8
9
  from griptape_nodes.retained_mode.events.base_events import (
@@ -744,7 +745,7 @@ class SendNodeMessageRequest(RequestPayload):
744
745
  """
745
746
 
746
747
  message_type: str
747
- message: Any
748
+ message: NodeMessagePayload | None
748
749
  node_name: str | None = None
749
750
  optional_element_name: str | None = None
750
751
 
@@ -758,7 +759,7 @@ class SendNodeMessageResultSuccess(ResultPayloadSuccess):
758
759
  response: Optional response data from the node's message handler
759
760
  """
760
761
 
761
- response: Any = None
762
+ response: NodeMessagePayload | None = None
762
763
 
763
764
 
764
765
  @dataclass
@@ -773,7 +774,7 @@ class SendNodeMessageResultFailure(ResultPayloadFailure):
773
774
  response: Optional response data from the node's message handler (even on failure)
774
775
  """
775
776
 
776
- response: Any = None
777
+ response: NodeMessagePayload | None = None
777
778
 
778
779
 
779
780
  @dataclass
@@ -37,6 +37,7 @@ from griptape_nodes.retained_mode.events.app_events import (
37
37
  )
38
38
  from griptape_nodes.retained_mode.events.base_events import (
39
39
  GriptapeNodeEvent,
40
+ ResultDetails,
40
41
  ResultPayloadFailure,
41
42
  )
42
43
  from griptape_nodes.retained_mode.events.flow_events import (
@@ -252,7 +253,9 @@ class GriptapeNodes(metaclass=SingletonMeta):
252
253
  type(request).__name__,
253
254
  request,
254
255
  )
255
- return ResultPayloadFailure(exception=e)
256
+ return ResultPayloadFailure(
257
+ exception=e, result_details=f"Unhandled exception while processing {type(request).__name__}: {e}"
258
+ )
256
259
  else:
257
260
  return result_event.result
258
261
 
@@ -276,7 +279,9 @@ class GriptapeNodes(metaclass=SingletonMeta):
276
279
  type(request).__name__,
277
280
  request,
278
281
  )
279
- return ResultPayloadFailure(exception=e)
282
+ return ResultPayloadFailure(
283
+ exception=e, result_details=f"Unhandled exception while processing async {type(request).__name__}: {e}"
284
+ )
280
285
  else:
281
286
  return result_event.result
282
287
 
@@ -421,6 +426,7 @@ class GriptapeNodes(metaclass=SingletonMeta):
421
426
  major=engine_ver.major,
422
427
  minor=engine_ver.minor,
423
428
  patch=engine_ver.patch,
429
+ result_details="Engine version retrieved successfully.",
424
430
  )
425
431
  details = f"Attempted to get engine version. Failed because version string '{engine_ver}' wasn't in expected major.minor.patch format."
426
432
  logger.error(details)
@@ -447,7 +453,7 @@ class GriptapeNodes(metaclass=SingletonMeta):
447
453
  await subscribe_to_topic(topic)
448
454
  logger.info("Subscribed to new session topic: %s", topic)
449
455
 
450
- return AppStartSessionResultSuccess(current_session_id)
456
+ return AppStartSessionResultSuccess(current_session_id, result_details="Session started successfully.")
451
457
 
452
458
  async def handle_session_end_request(self, _: AppEndSessionRequest) -> ResultPayload:
453
459
  from griptape_nodes.app.app import unsubscribe_from_topic
@@ -465,14 +471,19 @@ class GriptapeNodes(metaclass=SingletonMeta):
465
471
  unsubscribe_topic = f"sessions/{previous_session_id}/request"
466
472
  await unsubscribe_from_topic(unsubscribe_topic)
467
473
 
468
- return AppEndSessionResultSuccess(session_id=previous_session_id)
474
+ return AppEndSessionResultSuccess(
475
+ session_id=previous_session_id, result_details="Session ended successfully."
476
+ )
469
477
  except Exception as err:
470
478
  details = f"Failed to end session due to '{err}'."
471
479
  logger.error(details)
472
480
  return AppEndSessionResultFailure(result_details=details)
473
481
 
474
482
  def handle_get_session_request(self, _: AppGetSessionRequest) -> ResultPayload:
475
- return AppGetSessionResultSuccess(session_id=GriptapeNodes.SessionManager().get_active_session_id())
483
+ return AppGetSessionResultSuccess(
484
+ session_id=GriptapeNodes.SessionManager().get_active_session_id(),
485
+ result_details="Session ID retrieved successfully.",
486
+ )
476
487
 
477
488
  def handle_session_heartbeat_request(self, request: SessionHeartbeatRequest) -> ResultPayload: # noqa: ARG002
478
489
  """Handle session heartbeat requests.
@@ -487,8 +498,7 @@ class GriptapeNodes(metaclass=SingletonMeta):
487
498
  return SessionHeartbeatResultFailure(result_details=details)
488
499
 
489
500
  details = f"Session heartbeat successful for session: {active_session_id}"
490
- logger.debug(details)
491
- return SessionHeartbeatResultSuccess()
501
+ return SessionHeartbeatResultSuccess(result_details=details)
492
502
  except Exception as err:
493
503
  details = f"Failed to handle session heartbeat: {err}"
494
504
  logger.error(details)
@@ -509,7 +519,6 @@ class GriptapeNodes(metaclass=SingletonMeta):
509
519
  # Get engine name
510
520
  engine_name = GriptapeNodes.EngineIdentityManager().get_engine_name()
511
521
 
512
- logger.debug("Engine heartbeat successful")
513
522
  return EngineHeartbeatResultSuccess(
514
523
  heartbeat_id=request.heartbeat_id,
515
524
  engine_version=engine_version,
@@ -517,6 +526,7 @@ class GriptapeNodes(metaclass=SingletonMeta):
517
526
  engine_id=GriptapeNodes.EngineIdentityManager().get_active_engine_id(),
518
527
  session_id=GriptapeNodes.SessionManager().get_active_session_id(),
519
528
  timestamp=datetime.now(tz=UTC).isoformat(),
529
+ result_details="Engine heartbeat successful",
520
530
  **instance_info,
521
531
  **workflow_info,
522
532
  )
@@ -529,8 +539,9 @@ class GriptapeNodes(metaclass=SingletonMeta):
529
539
  """Handle requests to get the current engine name."""
530
540
  try:
531
541
  engine_name = GriptapeNodes.EngineIdentityManager().get_engine_name()
532
- logger.debug("Retrieved engine name: %s", engine_name)
533
- return GetEngineNameResultSuccess(engine_name=engine_name)
542
+ return GetEngineNameResultSuccess(
543
+ engine_name=engine_name, result_details="Engine name retrieved successfully."
544
+ )
534
545
  except Exception as err:
535
546
  error_message = f"Failed to get engine name: {err}"
536
547
  logger.error(error_message)
@@ -547,8 +558,10 @@ class GriptapeNodes(metaclass=SingletonMeta):
547
558
 
548
559
  # Set the new engine name
549
560
  GriptapeNodes.EngineIdentityManager().set_engine_name(request.engine_name.strip())
550
- logger.info("Engine name set to: %s", request.engine_name.strip())
551
- return SetEngineNameResultSuccess(engine_name=request.engine_name.strip())
561
+ details = f"Engine name set to: {request.engine_name.strip()}"
562
+ return SetEngineNameResultSuccess(
563
+ engine_name=request.engine_name.strip(), result_details=ResultDetails(message=details, level="INFO")
564
+ )
552
565
 
553
566
  except Exception as err:
554
567
  error_message = f"Failed to set engine name: {err}"
@@ -137,7 +137,7 @@ class AgentManager:
137
137
  if self.mcp_tool is None:
138
138
  self.mcp_tool = self._initialize_mcp_tool()
139
139
  await asyncio.to_thread(self._on_handle_run_agent_request, request)
140
- return RunAgentResultStarted()
140
+ return RunAgentResultStarted(result_details="Agent execution started successfully.")
141
141
 
142
142
  def _create_agent(self) -> Agent:
143
143
  output_schema = Schema(
@@ -207,7 +207,9 @@ class AgentManager:
207
207
  error=last_event.task_output.to_dict(), result_details=last_event.task_output.to_json()
208
208
  )
209
209
  if isinstance(last_event.task_output, JsonArtifact):
210
- return RunAgentResultSuccess(last_event.task_output.to_dict())
210
+ return RunAgentResultSuccess(
211
+ last_event.task_output.to_dict(), result_details="Agent execution completed successfully."
212
+ )
211
213
  err_msg = f"Unexpected final event: {last_event}"
212
214
  logger.error(err_msg)
213
215
  return RunAgentResultFailure(error=ErrorArtifact(last_event).to_dict(), result_details=err_msg)
@@ -226,7 +228,7 @@ class AgentManager:
226
228
  details = f"Error configuring agent: {e}"
227
229
  logger.error(details)
228
230
  return ConfigureAgentResultFailure(result_details=details)
229
- return ConfigureAgentResultSuccess()
231
+ return ConfigureAgentResultSuccess(result_details="Agent configured successfully.")
230
232
 
231
233
  def on_handle_reset_agent_conversation_memory_request(
232
234
  self, _: ResetAgentConversationMemoryRequest
@@ -237,7 +239,7 @@ class AgentManager:
237
239
  details = f"Error resetting agent conversation memory: {e}"
238
240
  logger.error(details)
239
241
  return ResetAgentConversationMemoryResultFailure(result_details=details)
240
- return ResetAgentConversationMemoryResultSuccess()
242
+ return ResetAgentConversationMemoryResultSuccess(result_details="Agent conversation memory reset successfully.")
241
243
 
242
244
  def on_handle_get_conversation_memory_request(self, _: GetConversationMemoryRequest) -> ResultPayload:
243
245
  try:
@@ -246,4 +248,6 @@ class AgentManager:
246
248
  details = f"Error getting conversation memory: {e}"
247
249
  logger.error(details)
248
250
  return GetConversationMemoryResultFailure(result_details=details)
249
- return GetConversationMemoryResultSuccess(runs=conversation_memory)
251
+ return GetConversationMemoryResultSuccess(
252
+ runs=conversation_memory, result_details="Conversation memory retrieved successfully."
253
+ )
@@ -43,7 +43,9 @@ class ArbitraryCodeExecManager:
43
43
  python_output = exec(request.python_string) # noqa: S102
44
44
 
45
45
  captured_output = strip_ansi_codes(string_buffer.getvalue())
46
- result = RunArbitraryPythonStringResultSuccess(python_output=captured_output)
46
+ result = RunArbitraryPythonStringResultSuccess(
47
+ python_output=captured_output, result_details="Successfully executed Python string"
48
+ )
47
49
  except Exception as e:
48
50
  python_output = f"ERROR: {e}"
49
51
  result = RunArbitraryPythonStringResultFailure(python_output=python_output, result_details=python_output)
@@ -279,19 +279,20 @@ class ContextManager:
279
279
  # As of today, we only allow a single Workflow context at a time. This may change in the future.
280
280
  if self.has_current_workflow():
281
281
  msg = f"Attempted to set the Workflow '{request.workflow_name}' as the Current Context. Failed because an existing workflow, '{self.get_current_workflow_name()}', is already in the Current Context. In order to clear the existing workflow and remove all objects and references to it, issue a ClearAllObjectState request."
282
- logger.error(msg)
283
- return SetWorkflowContextFailure()
282
+ return SetWorkflowContextFailure(result_details=msg)
284
283
 
285
284
  self.push_workflow(request.workflow_name)
286
285
  msg = f"Successfully set the Workflow '{request.workflow_name}' as the Current Context."
287
- logger.debug(msg)
288
- return SetWorkflowContextSuccess()
286
+ return SetWorkflowContextSuccess(result_details=msg)
289
287
 
290
288
  def on_get_workflow_context_request(self, request: GetWorkflowContextRequest) -> ResultPayload: # noqa: ARG002
291
289
  workflow_name = None
292
290
  if self.has_current_workflow():
293
291
  workflow_name = self.get_current_workflow_name()
294
- return GetWorkflowContextSuccess(workflow_name=workflow_name)
292
+ return GetWorkflowContextSuccess(
293
+ workflow_name=workflow_name,
294
+ result_details=f"Successfully retrieved workflow context: {workflow_name or 'None'}",
295
+ )
295
296
 
296
297
  def workflow(self, workflow_name: str) -> ContextManager.WorkflowContext:
297
298
  """Create a context manager for a Workflow context.