@outputai/core 0.5.1-next.8e45051.0 → 0.5.1-next.93f660c.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@outputai/core",
3
- "version": "0.5.1-next.8e45051.0",
3
+ "version": "0.5.1-next.93f660c.0",
4
4
  "description": "The core module of the output framework",
5
5
  "type": "module",
6
6
  "exports": {
@@ -4,7 +4,7 @@ import { serializeError } from './tools/utils.js';
4
4
  import { isStringboolTrue } from '#utils';
5
5
  import * as localProcessor from './processors/local/index.js';
6
6
  import * as s3Processor from './processors/s3/index.js';
7
- import { ComponentType } from '#consts';
7
+ import { ComponentType, Signal } from '#consts';
8
8
  import { createChildLogger } from '#logger';
9
9
  import { EventAction } from './trace_consts.js';
10
10
  import { BaseAttribute } from './trace_attribute.js';
@@ -93,14 +93,14 @@ export const addEventAction = ( action, { kind, name, id, parentId, details, exe
93
93
  export function addEventActionWithContext( action, options ) {
94
94
  const storeContent = Storage.load();
95
95
  if ( storeContent ) { // If there is no storageContext this was not called from a Temporal environment
96
- const { parentId, executionContext, sendAttributeSignal } = storeContent;
96
+ const { parentId, parentName, executionContext, workflowHandle } = storeContent;
97
97
  if ( action === EventAction.ADD_ATTR ) {
98
98
  const attribute = options.details;
99
99
  if ( !( attribute instanceof BaseAttribute ) ) {
100
- throw new Error( `Event ${EventAction.ADD_ATTR} argument is not a BaseAttribute instance` );
101
- } else {
102
- sendAttributeSignal( options.details );
100
+ throw new Error( `${EventAction.ADD_ATTR} called argument that is not a BaseAttribute instance` );
103
101
  }
102
+ attribute.setActivity( parentId, parentName );
103
+ workflowHandle.signal( Signal.ADD_ATTRIBUTE, attribute );
104
104
  }
105
105
  addEventAction( action, { ...options, parentId, executionContext } );
106
106
  }
@@ -5,12 +5,6 @@ vi.mock( '#async_storage', () => ( {
5
5
  Storage: { load: storageLoadMock }
6
6
  } ) );
7
7
 
8
- const logWarnMock = vi.fn();
9
- const logErrorMock = vi.fn();
10
- vi.mock( '#logger', () => ( {
11
- createChildLogger: () => ( { warn: logWarnMock, error: logErrorMock } )
12
- } ) );
13
-
14
8
  const localInitMock = vi.fn( async () => {} );
15
9
  const localExecMock = vi.fn();
16
10
  const localGetDestinationMock = vi.fn( () => '/local/path.json' );
@@ -123,62 +117,6 @@ describe( 'tracing/trace_engine', () => {
123
117
  expect( payload.entry.action ).toBe( 'tick' );
124
118
  } );
125
119
 
126
- it( 'addEventActionWithContext() sends ADD_ATTR attributes through storage context', async () => {
127
- process.env.OUTPUT_TRACE_LOCAL_ON = 'true';
128
- const sendAttributeSignalMock = vi.fn();
129
- const executionContext = { runId: 'r1', disableTrace: false };
130
- storageLoadMock.mockReturnValue( {
131
- parentId: 'ctx-p',
132
- executionContext,
133
- sendAttributeSignal: sendAttributeSignalMock
134
- } );
135
- const { init, addEventActionWithContext } = await loadTraceEngine();
136
- const { EventAction } = await import( './trace_consts.js' );
137
- const { Attribute } = await import( './trace_attribute.js' );
138
- await init();
139
-
140
- const attribute = new Attribute.HTTPRequestCount( 'https://example.test', 'req-1' );
141
- addEventActionWithContext( EventAction.ADD_ATTR, { kind: 'http', name: 'request', id: 'req-1', details: attribute } );
142
-
143
- expect( sendAttributeSignalMock ).toHaveBeenCalledTimes( 1 );
144
- expect( sendAttributeSignalMock ).toHaveBeenCalledWith( attribute );
145
- expect( localExecMock ).toHaveBeenCalledTimes( 1 );
146
- expect( localExecMock.mock.calls[0][0] ).toEqual( {
147
- executionContext,
148
- entry: {
149
- kind: 'http',
150
- action: EventAction.ADD_ATTR,
151
- name: 'request',
152
- id: 'req-1',
153
- parentId: 'ctx-p',
154
- timestamp: expect.any( Number ),
155
- details: attribute
156
- }
157
- } );
158
- } );
159
-
160
- it( 'addEventActionWithContext() throws on invalid ADD_ATTR signal payloads', async () => {
161
- process.env.OUTPUT_TRACE_LOCAL_ON = 'true';
162
- const sendAttributeSignalMock = vi.fn();
163
- storageLoadMock.mockReturnValue( {
164
- parentId: 'ctx-p',
165
- executionContext: { runId: 'r1', disableTrace: false },
166
- sendAttributeSignal: sendAttributeSignalMock
167
- } );
168
- const { init, addEventActionWithContext } = await loadTraceEngine();
169
- const { EventAction } = await import( './trace_consts.js' );
170
- await init();
171
-
172
- const invalidAttribute = { type: 'not-a-base-attribute' };
173
- expect( () => addEventActionWithContext(
174
- EventAction.ADD_ATTR,
175
- { kind: 'http', name: 'request', id: 'req-1', details: invalidAttribute }
176
- ) ).toThrow( /not a BaseAttribute instance/ );
177
-
178
- expect( sendAttributeSignalMock ).not.toHaveBeenCalled();
179
- expect( localExecMock ).not.toHaveBeenCalled();
180
- } );
181
-
182
120
  it( 'addEventActionWithContext() does not emit when storage executionContext.disableTrace is true', async () => {
183
121
  process.env.OUTPUT_TRACE_LOCAL_ON = '1';
184
122
  storageLoadMock.mockReturnValue( {
@@ -132,16 +132,3 @@ export function deepMerge( a: object, b: object ): object;
132
132
  * @returns Short string using A–Z, a–z, 0–9, `_`, `-` (typically 21–22 chars).
133
133
  */
134
134
  export function toUrlSafeBase64( uuid: string ): string;
135
-
136
- /**
137
- * Similar to native Promise.allSettled, but rejects with `{ isTimeout: true }`
138
- * if the execution exceeds the given timeout.
139
- *
140
- * @param promises - Values or promises to wait for.
141
- * @param timeoutMs - Maximum wait time in milliseconds.
142
- * @returns Native Promise.allSettled-style results.
143
- */
144
- export function allSettledWithTimeout<T>(
145
- promises: Array<T | PromiseLike<T>>,
146
- timeoutMs: number
147
- ): Promise<PromiseSettledResult<Awaited<T>>[]>;
@@ -209,42 +209,3 @@ export const toUrlSafeBase64 = uuid => {
209
209
  const toDigits = n => n <= 0n ? [] : toDigits( n / base ).concat( alphabet[Number( n % base )] );
210
210
  return toDigits( BigInt( '0x' + hex ) ).join( '' );
211
211
  };
212
-
213
- /**
214
- * Similar to native Promise.allSettled but throws an Error if the execution exceeds a given time.
215
- *
216
- * The error thrown will have attribute `.isTimeout` as `true`.
217
- *
218
- * @template T
219
- * @param {Array<T | PromiseLike<T>>} promises
220
- * @param {number} timeoutMs
221
- * @returns {Promise<PromiseSettledResult<Awaited<T>>[]>}
222
- * @throws {Error & { isTimeout: true }}
223
- */
224
- export const allSettledWithTimeout = ( () => {
225
- class TimeoutError extends Error {
226
- isTimeout = true;
227
- constructor() {
228
- super( 'Timed out before completing all promises' );
229
- }
230
- }
231
-
232
- return async ( promises, timeoutMs ) => {
233
- if ( promises.length === 0 ) {
234
- return [];
235
- }
236
-
237
- const state = { timeoutMonitor: null };
238
-
239
- try {
240
- return await Promise.race( [
241
- Promise.allSettled( promises ),
242
- new Promise( ( _, reject ) => {
243
- state.timeoutMonitor = setTimeout( () => reject( new TimeoutError() ), timeoutMs );
244
- } )
245
- ] );
246
- } finally {
247
- clearTimeout( state.timeoutMonitor );
248
- }
249
- };
250
- } )();
@@ -1,4 +1,4 @@
1
- import { describe, it, expect, vi } from 'vitest';
1
+ import { describe, it, expect } from 'vitest';
2
2
  import { Readable } from 'node:stream';
3
3
  import {
4
4
  clone,
@@ -6,8 +6,7 @@ import {
6
6
  serializeFetchResponse,
7
7
  deepMerge,
8
8
  isPlainObject,
9
- toUrlSafeBase64,
10
- allSettledWithTimeout
9
+ toUrlSafeBase64
11
10
  } from './utils.js';
12
11
 
13
12
  describe( 'clone', () => {
@@ -23,44 +22,6 @@ describe( 'clone', () => {
23
22
  } );
24
23
  } );
25
24
 
26
- describe( 'allSettledWithTimeout', () => {
27
- it( 'returns an empty array when no promises are provided', async () => {
28
- await expect( allSettledWithTimeout( [], 100 ) ).resolves.toEqual( [] );
29
- } );
30
-
31
- it( 'returns native allSettled results when all promises settle before timeout', async () => {
32
- const error = new Error( 'boom' );
33
- const result = await allSettledWithTimeout( [
34
- Promise.resolve( 'ok' ),
35
- Promise.reject( error ),
36
- 42
37
- ], 100 );
38
-
39
- expect( result ).toEqual( [
40
- { status: 'fulfilled', value: 'ok' },
41
- { status: 'rejected', reason: error },
42
- { status: 'fulfilled', value: 42 }
43
- ] );
44
- } );
45
-
46
- it( 'rejects with a timeout-shaped error when promises do not settle in time', async () => {
47
- vi.useFakeTimers();
48
- try {
49
- const pending = new Promise( () => {} );
50
- const result = allSettledWithTimeout( [ pending ], 1000 );
51
- const assertion = expect( result ).rejects.toMatchObject( {
52
- isTimeout: true,
53
- message: 'Timed out before completing all promises'
54
- } );
55
-
56
- await vi.advanceTimersByTimeAsync( 1000 );
57
- await assertion;
58
- } finally {
59
- vi.useRealTimers();
60
- }
61
- } );
62
- } );
63
-
64
25
  describe( 'serializeFetchResponse', () => {
65
26
  it( 'serializes JSON response body and flattens headers', async () => {
66
27
  const payload = { a: 1, b: 'two' };
@@ -2,16 +2,10 @@ import { Context } from '@temporalio/activity';
2
2
  import { Storage } from '#async_storage';
3
3
  import * as Tracing from '#tracing';
4
4
  import { headersToObject } from '../sandboxed_utils.js';
5
- import { BusEventType, METADATA_ACCESS_SYMBOL, Signal } from '#consts';
5
+ import { BusEventType, METADATA_ACCESS_SYMBOL } from '#consts';
6
6
  import { activityHeartbeatEnabled, activityHeartbeatIntervalMs, namespace } from '../configs.js';
7
7
  import { messageBus } from '#bus';
8
8
  import { Client } from '@temporalio/client';
9
- import { createChildLogger } from '#logger';
10
- import { allSettledWithTimeout } from '#utils';
11
-
12
- const log = createChildLogger( 'ActivityInterceptor' );
13
-
14
- const IN_FLIGHT_SIGNALS_TIMEOUT_MS = 30_000;
15
9
 
16
10
  /*
17
11
  This interceptor wraps every activity execution with cross-cutting concerns:
@@ -42,20 +36,6 @@ export class ActivityExecutionInterceptor {
42
36
  this.connection = connection;
43
37
  };
44
38
 
45
- /**
46
- * Returns a workflow entry by its name or throws error
47
- * @param {string} workflowName
48
- * @returns {object} Workflow entry
49
- * @throws {Error}
50
- */
51
- getWorkflowEntry( workflowName ) {
52
- const workflowEntry = this.workflowsMap.get( workflowName );
53
- if ( !workflowEntry ) {
54
- throw new Error( `Activity interceptor: workflow "${workflowName}" not found in workflowsMap.` );
55
- }
56
- return workflowEntry;
57
- }
58
-
59
39
  async execute( input, next ) {
60
40
  const startDate = Date.now();
61
41
  const client = new Client( { connection: this.connection, namespace } );
@@ -63,66 +43,31 @@ export class ActivityExecutionInterceptor {
63
43
  const { workflowExecution: { workflowId }, activityId: id, activityType: name, workflowType: workflowName } = Context.current().info;
64
44
  const { executionContext } = headersToObject( input.headers );
65
45
  const { type: kind } = this.activities?.[name]?.[METADATA_ACCESS_SYMBOL];
66
- const { path: workflowFilename } = this.getWorkflowEntry( workflowName );
67
46
 
68
47
  const workflowHandle = client.workflow.getHandle( workflowId );
69
48
 
70
- const state = {
71
- heartbeat: null,
72
- activityOutput: undefined,
73
- signals: []
74
- };
75
-
76
- const errorContext = {
77
- workflowId,
78
- workflowName,
79
- activityId: id,
80
- activityName: name
81
- };
82
-
83
- const sendAttributeSignal = attribute => {
84
- attribute.setActivity( id, name );
85
- state.signals.push(
86
- workflowHandle
87
- .signal( Signal.ADD_ATTRIBUTE, attribute )
88
- .catch( e =>
89
- log.warn( `Signal "${Signal.ADD_ATTRIBUTE}" failed`, { message: e.message, stack: e.stack, activityId: id, ...errorContext } )
90
- )
91
- );
92
- };
93
-
94
- const flushSignals = async signals => {
95
- try {
96
- await allSettledWithTimeout( signals, IN_FLIGHT_SIGNALS_TIMEOUT_MS );
97
- } catch ( error ) {
98
- if ( error.isTimeout ) {
99
- log.warn( 'Some usage/cost attributes were missed because not all activity signals were sent to the workflow', errorContext );
100
- } else {
101
- throw error;
102
- }
103
- }
104
- };
105
-
106
- // Wraps the execution with accessible metadata for the activity
107
- const ctx = { parentId: id, executionContext, workflowFilename, sendAttributeSignal };
108
-
109
49
  messageBus.emit( BusEventType.ACTIVITY_START, { id, name, kind, workflowId, workflowName } );
110
50
  Tracing.addEventStart( { id, name, kind, parentId: workflowId, details: input.args[0], executionContext } );
111
51
 
52
+ const workflowEntry = this.workflowsMap.get( workflowName );
53
+ if ( !workflowEntry ) {
54
+ const availableWorkflows = [ ...this.workflowsMap.keys() ].join( ', ' );
55
+ throw new Error( `Activity interceptor: workflow "${workflowName}" not found in workflowsMap. Available: [${availableWorkflows}]` );
56
+ }
57
+ const workflowFilename = workflowEntry.path;
58
+
59
+ const intervals = { heartbeat: null };
112
60
  try {
113
61
  // Sends heartbeat to communicate that activity is still alive
114
- state.heartbeat = activityHeartbeatEnabled && setInterval( () => Context.current().heartbeat(), activityHeartbeatIntervalMs );
62
+ intervals.heartbeat = activityHeartbeatEnabled && setInterval( () => Context.current().heartbeat(), activityHeartbeatIntervalMs );
115
63
 
116
- try {
117
- state.activityOutput = await Storage.runWithContext( async _ => next( input ), ctx );
118
- } finally {
119
- // Ensure in-flight signals are delivered (up to a reasonable time) before handling errors
120
- await flushSignals( state.signals );
121
- }
64
+ // Wraps the execution with accessible metadata for the activity
65
+ const ctx = { parentId: id, parentName: name, executionContext, workflowFilename, workflowHandle };
66
+ const output = await Storage.runWithContext( async _ => next( input ), ctx );
122
67
 
123
68
  messageBus.emit( BusEventType.ACTIVITY_END, { id, name, kind, workflowId, workflowName, duration: Date.now() - startDate } );
124
- Tracing.addEventEnd( { id, details: state.activityOutput, executionContext } );
125
- return state.activityOutput;
69
+ Tracing.addEventEnd( { id, details: output, executionContext } );
70
+ return output;
126
71
 
127
72
  } catch ( error ) {
128
73
  messageBus.emit( BusEventType.ACTIVITY_ERROR, { id, name, kind, workflowId, workflowName, duration: Date.now() - startDate, error } );
@@ -130,7 +75,7 @@ export class ActivityExecutionInterceptor {
130
75
 
131
76
  throw error;
132
77
  } finally {
133
- clearInterval( state.heartbeat );
78
+ clearInterval( intervals.heartbeat );
134
79
  }
135
80
  }
136
81
  };
@@ -1,12 +1,10 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import { BusEventType, Signal } from '#consts';
2
+ import { BusEventType } from '#consts';
3
3
 
4
4
  const METADATA_ACCESS_SYMBOL = vi.hoisted( () => Symbol( '__metadata' ) );
5
5
  const workflowHandleMock = vi.hoisted( () => ( { signal: vi.fn() } ) );
6
6
  const getHandleMock = vi.hoisted( () => vi.fn( () => workflowHandleMock ) );
7
7
  const clientConstructorMock = vi.hoisted( () => vi.fn() );
8
- const allSettledWithTimeoutMock = vi.hoisted( () => vi.fn().mockResolvedValue( [] ) );
9
- const logWarnMock = vi.hoisted( () => vi.fn() );
10
8
 
11
9
  const heartbeatMock = vi.fn();
12
10
  const runWithContextMock = vi.hoisted( () => vi.fn().mockImplementation( async fn => fn() ) );
@@ -44,15 +42,6 @@ vi.mock( '#async_storage', () => ( {
44
42
  }
45
43
  } ) );
46
44
 
47
- vi.mock( '#utils', async importOriginal => {
48
- const actual = await importOriginal();
49
- return { ...actual, allSettledWithTimeout: allSettledWithTimeoutMock };
50
- } );
51
-
52
- vi.mock( '#logger', () => ( {
53
- createChildLogger: () => ( { warn: logWarnMock } )
54
- } ) );
55
-
56
45
  const addEventStartMock = vi.fn();
57
46
  const addEventEndMock = vi.fn();
58
47
  const addEventErrorMock = vi.fn();
@@ -104,8 +93,6 @@ const makeInput = () => ( {
104
93
  describe( 'ActivityExecutionInterceptor', () => {
105
94
  beforeEach( () => {
106
95
  vi.clearAllMocks();
107
- allSettledWithTimeoutMock.mockResolvedValue( [] );
108
- workflowHandleMock.signal.mockResolvedValue( undefined );
109
96
  vi.useFakeTimers();
110
97
  vi.resetModules();
111
98
  // Default: heartbeat enabled with 50ms interval for fast tests
@@ -142,83 +129,15 @@ describe( 'ActivityExecutionInterceptor', () => {
142
129
  expect.any( Function ),
143
130
  expect.objectContaining( {
144
131
  parentId: 'act-1',
132
+ parentName: 'myWorkflow#myStep',
145
133
  executionContext: { workflowId: 'wf-1' },
146
134
  workflowFilename: '/workflows/myWorkflow.js',
147
- sendAttributeSignal: expect.any( Function )
135
+ workflowHandle: workflowHandleMock
148
136
  } )
149
137
  );
150
138
  expect( getHandleMock ).toHaveBeenCalledWith( 'wf-1' );
151
139
  } );
152
140
 
153
- it( 'handles next returning a non-Promise value', async () => {
154
- const { ActivityExecutionInterceptor } = await import( './activity.js' );
155
- const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows: makeWorkflows() } );
156
- const next = vi.fn( () => ( { result: 'sync' } ) );
157
-
158
- await expect( interceptor.execute( makeInput(), next ) ).resolves.toEqual( { result: 'sync' } );
159
-
160
- expect( allSettledWithTimeoutMock ).toHaveBeenCalledWith( [], 30_000 );
161
- expect( messageBusEmitMock ).toHaveBeenCalledWith( BusEventType.ACTIVITY_END, expect.any( Object ) );
162
- expect( addEventEndMock ).toHaveBeenCalledWith( { id: 'act-1', details: { result: 'sync' }, executionContext: { workflowId: 'wf-1' } } );
163
- expect( addEventErrorMock ).not.toHaveBeenCalled();
164
- } );
165
-
166
- it( 'handles signal flush timeout after successful execution', async () => {
167
- const timeoutError = Object.assign( new Error( 'timeout' ), { isTimeout: true } );
168
- allSettledWithTimeoutMock.mockRejectedValueOnce( timeoutError );
169
- const { ActivityExecutionInterceptor } = await import( './activity.js' );
170
- const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows: makeWorkflows() } );
171
- const next = vi.fn().mockResolvedValue( { result: 'ok' } );
172
-
173
- await expect( interceptor.execute( makeInput(), next ) ).resolves.toEqual( { result: 'ok' } );
174
-
175
- expect( allSettledWithTimeoutMock ).toHaveBeenCalledWith( [], 30_000 );
176
- expect( logWarnMock ).toHaveBeenCalledWith(
177
- 'Some usage/cost attributes were missed because not all activity signals were sent to the workflow',
178
- { workflowId: 'wf-1', workflowName: 'myWorkflow', activityId: 'act-1', activityName: 'myWorkflow#myStep' }
179
- );
180
- expect( messageBusEmitMock ).toHaveBeenCalledWith( BusEventType.ACTIVITY_END, expect.any( Object ) );
181
- expect( addEventEndMock ).toHaveBeenCalledOnce();
182
- expect( addEventErrorMock ).not.toHaveBeenCalled();
183
- } );
184
-
185
- it( 'handles signal flush timeout after failed execution', async () => {
186
- const timeoutError = Object.assign( new Error( 'timeout' ), { isTimeout: true } );
187
- allSettledWithTimeoutMock.mockRejectedValueOnce( timeoutError );
188
- const { ActivityExecutionInterceptor } = await import( './activity.js' );
189
- const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows: makeWorkflows() } );
190
- const error = new Error( 'step failed' );
191
- const next = vi.fn().mockRejectedValue( error );
192
-
193
- await expect( interceptor.execute( makeInput(), next ) ).rejects.toThrow( 'step failed' );
194
-
195
- expect( allSettledWithTimeoutMock ).toHaveBeenCalledWith( [], 30_000 );
196
- expect( logWarnMock ).toHaveBeenCalledWith(
197
- 'Some usage/cost attributes were missed because not all activity signals were sent to the workflow',
198
- { workflowId: 'wf-1', workflowName: 'myWorkflow', activityId: 'act-1', activityName: 'myWorkflow#myStep' }
199
- );
200
- expect( messageBusEmitMock ).toHaveBeenCalledWith( BusEventType.ACTIVITY_ERROR, expect.objectContaining( { error } ) );
201
- expect( addEventErrorMock ).toHaveBeenCalledOnce();
202
- expect( addEventEndMock ).not.toHaveBeenCalled();
203
- } );
204
-
205
- it( 'exposes sendAttributeSignal in activity context', async () => {
206
- const attribute = { setActivity: vi.fn() };
207
- runWithContextMock.mockImplementationOnce( async ( fn, ctx ) => {
208
- ctx.sendAttributeSignal( attribute );
209
- return fn();
210
- } );
211
- const { ActivityExecutionInterceptor } = await import( './activity.js' );
212
- const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows: makeWorkflows() } );
213
- const next = vi.fn().mockResolvedValue( { result: 'ok' } );
214
-
215
- await expect( interceptor.execute( makeInput(), next ) ).resolves.toEqual( { result: 'ok' } );
216
-
217
- expect( attribute.setActivity ).toHaveBeenCalledWith( 'act-1', 'myWorkflow#myStep' );
218
- expect( workflowHandleMock.signal ).toHaveBeenCalledWith( Signal.ADD_ATTRIBUTE, attribute );
219
- expect( allSettledWithTimeoutMock ).toHaveBeenCalledWith( [ expect.any( Promise ) ], 30_000 );
220
- } );
221
-
222
141
  it( 'records trace error event on failed execution', async () => {
223
142
  const { ActivityExecutionInterceptor } = await import( './activity.js' );
224
143
  const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows: makeWorkflows() } );