Skip to main content

Activity System

Activities are external functions or services that workflows can execute. They enable workflows to interact with databases, APIs, message queues, and other systems.

What are Activities?

An activity is:

  • A reusable piece of logic
  • Executed outside the workflow logic
  • Potentially long-running or unreliable
  • Subject to retries and timeouts
  • Isolated from workflow state

Activities allow you to:

  • Make HTTP requests
  • Query databases
  • Send emails or SMS
  • Process files
  • Call external APIs
  • Perform complex calculations
  • Integrate with third-party services

Basic Activity Usage

- activity:
type: http.request # Activity type identifier
input_data: # Input parameters
method: GET
url: https://api.example.com/users/123
output_name: user_data # Store result
retry_policy: # Timeout & retry (Temporal runtime only)
timeout_sec: 30 # Per-attempt execution timeout
max_attempts: 3 # Total attempts (initial + retries)

Activity Parameters

Required

ParameterTypeDescription
typestringActivity type identifier (e.g., "http.request")

Optional

ParameterTypeDescription
versionstringActivity version (default: "1.0.0")
config_datadictStatic configuration (evaluated once)
input_datadictDynamic input (evaluated per execution)
output_namestringVariable to store activity result
output_datalistTransform result before storing
retry_policydictNested timeout/retry config (Temporal only): timeout_sec (per-attempt execution timeout), schedule_to_close_timeout_sec, heartbeat_timeout_sec, heartbeat_interval_sec (heartbeat cadence; heartbeating is enabled only when both heartbeat_timeout_sec and heartbeat_interval_sec are set), max_attempts (total attempts = initial + retries), initial_interval_sec, backoff_coefficient, maximum_interval_sec, non_retryable_error_types
execute_locallybooleanForce local execution (bypass Temporal). Overrides the activity's own default — see Local Execution
enable_cachebooleanEnable result caching
cache_policydictCache configuration

Built-in Activities

Moco ships 182 activities across 20 providers — HTTP, shell, SQL, email, Kafka, RabbitMQ, Google Drive, Kubernetes, browser automation, LLM and RAG activities, and the platform's own state, secret and deployment activities. The Activity Catalog indexes every one of them, and each provider has a reference page giving the input and output contract of its activities with worked examples.

A few you will reach for constantly:

HTTP Request

Make HTTP requests to external APIs — see HTTP Activities:

# GET request
- activity:
type: http.request
input_data:
method: GET
url: https://api.example.com/data?limit=10&offset=0
headers:
Accept: application/json
output_json: true
output_name: api_response
retry_policy:
timeout_sec: 30
# POST request with JSON body
- activity:
type: http.request
input_data:
method: POST
url: https://api.example.com/orders
json_data:
order_id: "{{ order_id }}"
items: "{{ items }}"
total: "{{ total }}"
output_name: create_response

Delay

Pause workflow execution — see Built-in Core:

- activity:
type: builtin.delay
input_data:
duration: 5s # seconds: 5s, minutes: 5min, hours: 1h30m

State Persistence

Store and retrieve values that outlive a single run — see State Store:

# Save state
- activity:
type: builtin.state.set_state
input_data:
namespace: "user-preferences"
key: "{{ user_id }}"
value:
theme: dark
notifications: true

# Load state
- activity:
type: builtin.state.get_state
input_data:
namespace: "user-preferences"
key: "{{ user_id }}"
output_name: preferences

Secret Management

Access secrets securely. Plaintext secrets never travel between activities.

Most activities that need a secret take a secret key instead of the secret itself — for example openai.chat.completions.apikey_secret_key, email.send.password_secret_key or sql.query.connection_string_secret_key. The activity looks the secret up and decrypts it internally, so nothing sensitive touches workflow context at all. A bare NAME resolves a user-scoped secret; global/NAME resolves a global one.

Where an activity does not yet support that (for example http.request.encrypted_auth_token), use builtin.secret.get, which returns the secret still encrypted — pass that blob straight to the activity, which decrypts it internally.

- activity:
type: builtin.secret.get
input_data:
secret_name: database_password
in_global_ns: false # optional; true reads the shared global namespace
expiration_seconds: 60 # optional; defaults to 60
output_name: db_password

The returned secret expires after expiration_seconds, so a long-running workflow must re-run builtin.secret.get rather than holding the result. The full list of secret-bearing fields, the encryption model and the reserved-namespace rules are in Secret Activities.

AI and retrieval

openai.chat.completions runs a single prompt-and-response; claude_agent.query runs an autonomous multi-turn agent with explicitly granted tools; the llama_index.* activities build and query a vector index over your own documents so an LLM can answer from them. See OpenAI, Claude Agent and LlamaIndex, with runnable examples in moco-examples/openai-demo/, moco-examples/claude-agent-demo/ and moco-examples/rag-demo/.

Files, messaging and browsers

gdrive.* reads and writes Google Drive files; kafka.*, rabbit.*, graphql.subscribe and websocket.subscribe connect a workflow to message buses and live feeds; playwright.* and selenium.* drive a real browser. See Google Drive, Kafka, RabbitMQ and Playwright.

Config vs Input Data

Activities support two types of parameters:

config_data (Static)

Evaluated once at workflow start. Use for:

  • API endpoints
  • Credentials
  • Fixed configuration
- activity:
type: custom.data_processor
config_data:
endpoint: https://processor.example.com
api_key: "{{ env.API_KEY }}"
timeout: 60
input_data:
data: "{{ batch_data }}"

input_data (Dynamic)

Evaluated each time the activity executes. Use for:

  • Request parameters
  • Data to process
  • Dynamic values
- iteration:
input_data: "{{ user_ids }}"
body:
activity:
type: fetch.user
config_data:
api_base: https://api.example.com
input_data:
user_id: "{{ iter_item }}" # Different each iteration
output_name: user

Retry and Timeout

Timeout

Set maximum execution time:

- activity:
type: http.request
input_data:
url: https://slow-api.com/data
retry_policy:
timeout_sec: 10 # Timeout after 10 seconds
output_name: result

Retry

Configure automatic retries on failure:

- activity:
type: http.request
input_data:
url: https://unreliable-api.com/data
retry_policy:
max_attempts: 5 # Total attempts (initial + retries)
timeout_sec: 10 # Per-attempt timeout
output_name: result

Retry behavior:

  • Initial attempt + retries = total attempts
  • Exponential backoff between retries
  • Failures are logged
  • Final failure propagates to workflow

Caching

Enable caching to avoid redundant executions:

- activity:
type: http.request
input_data:
url: https://api.example.com/reference-data
enable_cache: true
cache_policy:
ttl_seconds: 3600 # Cache for 1 hour
cache_key: "ref-data-{{ date }}"
output_name: cached_data

Cache benefits:

  • Reduce API calls
  • Improve performance
  • Lower costs
  • Consistent data within TTL

Local Execution

Force activities to run in the workflow process (bypass Temporal workers):

- activity:
type: builtin.delay
input_data:
duration: 1s
execute_locally: true # Run in workflow process

Use for:

  • Very fast operations (< 1ms)
  • Operations that don't benefit from retries
  • Reducing worker overhead
  • Testing/debugging

Caution: Local activities:

  • Don't get automatic retries
  • Run in workflow process (blocking)
  • Can impact workflow performance

Activities that are already local by default

Some activities set execute_locally for you, so you don't have to write it. Short built-ins like builtin.now and builtin.delay do it because a queue round trip would cost more than the work itself.

All browser automation activities — every selenium.* and playwright.* type — default to local execution, and for a different reason: they need to stay on the same worker as the workflow.

A browser session belongs to the process that opened it. The session_id you get back from browser.create is only meaningful there, so if nav.goto or element.click ran somewhere else, they would not find the browser. Running these activities locally keeps a whole session — from browser.create to browser.close — on one worker, so a multi-step browser script works the way you'd expect.

Don't set execute_locally: false on a browser activity. The workflow will still validate and start, but the session will no longer be pinned to one worker and any step after browser.create can fail with an unknown session. See Playwright and Selenium for the full session model.

Conversely, an activity served by a different worker type can never run locally. claude_agent.query is the only one today: it runs on the agent worker, so execute_locally has no effect on it.

Output Transformation

Transform activity results before storing:

- activity:
type: http.request
input_data:
url: https://api.example.com/users
output_data:
- users: "{{ _raw_output }}" # Raw response
- user_count: "{{ len(users) }}" # Derived value
- first_user: "{{ users[0] if users else None }}"
output_name: result

The transformed data is stored in result:

{
"users": [...],
"user_count": 5,
"first_user": {...}
}

Custom Activities

Create custom activities for your specific needs:

1. Define Activity Provider

from moco.core.workflow.activity.activity_types import IActivityProvider

class EmailActivityProvider(IActivityProvider):
async def execute(
self,
config_data: dict,
input_data: dict,
context: dict
) -> dict:
# Send email
to = input_data['to']
subject = input_data['subject']
body = input_data['body']

# ... email sending logic ...

return {
'sent': True,
'message_id': 'msg-12345'
}

def get_manifest(self) -> dict:
return {
'type': 'myorg.send_email',
'version': '1.0.0',
'description': 'Send email via SMTP',
'input_schema': {
'to': {'type': 'string', 'required': True},
'subject': {'type': 'string', 'required': True},
'body': {'type': 'string', 'required': True},
}
}

2. Register Activity

from moco.core.workflow.activity.activity_directory import ActivityDirectory

activity_dir = ActivityDirectory()
activity_dir.register_provider('myorg.send_email', EmailActivityProvider())

3. Use in Workflow

- activity:
type: myorg.send_email
version: 1.0.0
config_data:
smtp_host: smtp.example.com
smtp_port: 587
input_data:
to: "{{ customer.email }}"
subject: "Order Confirmation #{{ order_id }}"
body: "{{ email_template }}"
output_name: email_result

Activity Best Practices

Error Handling

# Provide meaningful timeout
- activity:
type: external.api
input_data:
url: "{{ endpoint }}"
retry_policy:
timeout_sec: 30
max_attempts: 3
output_name: result

# Check for errors
- abort:
condition: "{{ not result.success }}"
type: raise
message: "API call failed: {{ result.error }}"

Idempotency

Design activities to be idempotent (safe to retry):

async def execute(self, config_data, input_data, context):
# Check if already processed
order_id = input_data['order_id']
if await self.is_processed(order_id):
return await self.get_previous_result(order_id)

# Process order
result = await self.process_order(order_id)

# Store result
await self.save_result(order_id, result)

return result

Use config_data for Static Values

# Good: Static values in config_data
- activity:
config_data:
api_base: https://api.example.com
api_key: "{{ env.API_KEY }}"
input_data:
user_id: "{{ user_id }}" # Dynamic per execution

# Bad: Everything in input_data
- activity:
input_data:
api_base: https://api.example.com # Same every time
api_key: "{{ env.API_KEY }}" # Same every time
user_id: "{{ user_id }}"

Appropriate Timeouts

# Fast operations
- activity:
type: builtin.delay
input_data:
duration: 1s
retry_policy:
timeout_sec: 2

# Slow operations
- activity:
type: data.process_large_file
input_data:
file_path: "{{ file }}"
retry_policy:
timeout_sec: 300 # 5 minutes

Next Steps