Temporal Foundation

Temporal Foundation

Resolute is built on Temporal, a durable execution platform. Understanding this foundation helps you leverage Resolute effectively and know when to reach for lower-level Temporal features.

What is Temporal?

Temporal is a platform for building reliable distributed systems. It provides:

  • Durable Execution - Workflow state survives process crashes and restarts
  • Event Sourcing - Complete history of every execution step
  • Automatic Retries - Failed activities retry with configurable policies
  • Visibility - Search and inspect running/completed workflows
  • Scalability - Distribute work across many workers

How Resolute Uses Temporal

┌─────────────────────────────────────────────────────────────────────┐
│                         Resolute Layer                               │
│                                                                      │
│   ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐          │
│   │   Flow   │  │   Node   │  │  State   │  │ Provider │          │
│   └────┬─────┘  └────┬─────┘  └────┬─────┘  └────┬─────┘          │
│        │             │             │             │                  │
│        │     Resolute abstractions compile to Temporal primitives   │
│        │             │             │             │                  │
│        ▼             ▼             ▼             ▼                  │
├─────────────────────────────────────────────────────────────────────┤
│                        Temporal SDK Layer                            │
│                                                                      │
│   ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐          │
│   │ Workflow │  │ Activity │  │ Context  │  │  Worker  │          │
│   └──────────┘  └──────────┘  └──────────┘  └──────────┘          │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘
                                   │
                                   │ gRPC
                                   ▼
┌─────────────────────────────────────────────────────────────────────┐
│                        Temporal Server                               │
│                                                                      │
│  ┌────────────────────────────────────────────────────────────┐    │
│  │  History Service  │  Matching Service  │  Frontend Service  │    │
│  └────────────────────────────────────────────────────────────┘    │
│                                                                      │
│  ┌────────────────────────────────────────────────────────────┐    │
│  │                    Persistence Layer                        │    │
│  │            (PostgreSQL, MySQL, Cassandra)                   │    │
│  └────────────────────────────────────────────────────────────┘    │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

Concept Mapping

Resolute ConceptTemporal EquivalentNotes
FlowWorkflowA Flow compiles to a Temporal workflow definition
Flow.ExecuteWorkflow functionThe function Temporal calls to run the workflow
NodeActivityNodes wrap activities with type safety
TriggerWorkflow start mechanismManual, Schedule, Signal map to Temporal features
FlowStateWorkflow context + local stateManages data passing between activities
ProviderActivity registrationsGroups related activities for registration
WorkerWorkerPolls Temporal for tasks
RetryPolicytemporal.RetryPolicyDirect mapping
TimeoutStartToCloseTimeoutActivity timeout

What Resolute Abstracts

1. Workflow Definition Boilerplate

Raw Temporal:

func MyWorkflow(ctx workflow.Context, input MyInput) error {
    ao := workflow.ActivityOptions{
        StartToCloseTimeout: 5 * time.Minute,
        RetryPolicy: &temporal.RetryPolicy{
            InitialInterval:    time.Second,
            BackoffCoefficient: 2.0,
            MaximumAttempts:    3,
        },
    }
    ctx = workflow.WithActivityOptions(ctx, ao)

    var result1 Activity1Output
    err := workflow.ExecuteActivity(ctx, Activity1, input).Get(ctx, &result1)
    if err != nil {
        return err
    }

    var result2 Activity2Output
    err = workflow.ExecuteActivity(ctx, Activity2, Activity2Input{
        Data: result1.Data,
    }).Get(ctx, &result2)
    if err != nil {
        return err
    }

    return nil
}

Resolute:

flow := core.NewFlow("my-workflow").
    TriggeredBy(core.Manual("api")).
    Then(core.NewNode("activity1", activity1, input).
        WithTimeout(5 * time.Minute).
        WithRetry(retryPolicy)).
    Then(core.NewNode("activity2", activity2, Activity2Input{}).
        WithInputFunc(func(state *core.FlowState) Activity2Input {
            result1 := core.Get[Activity1Output](state, "activity1")
            return Activity2Input{Data: result1.Data}
        })).
    Build()

2. Type-Safe Activity Inputs/Outputs

Raw Temporal:

// Must manually type assert
var result MyOutput
err := workflow.ExecuteActivity(ctx, myActivity, input).Get(ctx, &result)

Resolute:

// Compile-time type checking
node := core.NewNode[MyInput, MyOutput]("my-node", myActivity, input)
result := core.Get[MyOutput](state, "my-node")  // Type-safe retrieval

3. Activity Registration

Raw Temporal:

w := worker.New(c, "my-queue", worker.Options{})
w.RegisterWorkflow(MyWorkflow)
w.RegisterActivity(Activity1)
w.RegisterActivity(Activity2)
w.RegisterActivity(Activity3)
// ... for each activity

Resolute:

core.NewWorker().
    WithFlow(myFlow).
    WithProviders(myProvider).  // Registers all provider activities
    Run()

4. State Management

Raw Temporal:

// Must manage state passing manually
var result1 Out1
err := workflow.ExecuteActivity(ctx, act1, in1).Get(ctx, &result1)

// Transform for next activity
input2 := In2{Data: result1.Data}
var result2 Out2
err = workflow.ExecuteActivity(ctx, act2, input2).Get(ctx, &result2)

Resolute:

// FlowState handles automatically
// Each node's output is stored and accessible to subsequent nodes
processNode.WithInputFunc(func(state *core.FlowState) ProcessInput {
    fetch := core.Get[FetchOutput](state, "fetch")
    return ProcessInput{Items: fetch.Items}
})

What You Still Get from Temporal

Resolute preserves all Temporal guarantees:

Durable Execution

If a worker crashes mid-workflow:

  1. Temporal preserves the execution history
  2. Another worker picks up the workflow
  3. Execution resumes from the last completed activity

Automatic Retries

Activities automatically retry on failure:

node := core.NewNode("flaky", flakyActivity, input).
    WithRetry(core.RetryPolicy{
        InitialInterval:    time.Second,
        BackoffCoefficient: 2.0,
        MaximumInterval:    time.Minute,
        MaximumAttempts:    5,
    })

Temporal handles retry timing, backoff, and attempt tracking.

Event Sourcing

Every execution step is recorded:

Workflow Started: my-workflow (id: wf-123)
├── Activity Started: fetch-data
├── Activity Completed: fetch-data (output: {...})
├── Activity Started: process-data
├── Activity Completed: process-data (output: {...})
├── Activity Started: store-data
├── Activity Completed: store-data (output: {...})
Workflow Completed: my-workflow

View in Temporal UI at http://localhost:8233.

Query workflows via Temporal’s visibility API:

# List running workflows
temporal workflow list --query 'ExecutionStatus="Running"'

# Search by custom attribute
temporal workflow list --query 'WorkflowType="jira-sync"'

Scalability

Multiple workers can process the same queue:

Worker A ─┐
Worker B ─┼─── Task Queue ─── Temporal Server
Worker C ─┘

Temporal distributes tasks across available workers.

When to Use Raw Temporal

Resolute handles most use cases, but sometimes you need raw Temporal:

1. Child Workflows

For complex workflow hierarchies:

// In your workflow function, use raw Temporal
childRun := workflow.ExecuteChildWorkflow(ctx, ChildWorkflow, input)
var result ChildOutput
err := childRun.Get(ctx, &result)

2. Signals and Queries

For dynamic workflow interaction:

// Receive signals in workflow
signalChan := workflow.GetSignalChannel(ctx, "my-signal")
selector := workflow.NewSelector(ctx)
selector.AddReceive(signalChan, func(c workflow.ReceiveChannel, more bool) {
    var signal MySignal
    c.Receive(ctx, &signal)
    // Handle signal
})

3. Timers and Delays

For time-based logic:

// Sleep for a duration
workflow.Sleep(ctx, 5*time.Minute)

// Timer with selector
timerFuture := workflow.NewTimer(ctx, time.Hour)

4. Advanced Retry Logic

For custom retry handling:

// Custom retry with activity error inspection
for attempt := 0; attempt < maxAttempts; attempt++ {
    err := workflow.ExecuteActivity(ctx, myActivity, input).Get(ctx, &result)
    if err == nil {
        break
    }
    if !isRetryable(err) {
        return err
    }
    workflow.Sleep(ctx, backoff(attempt))
}

5. Workflow Versioning

For long-running workflow migrations:

v := workflow.GetVersion(ctx, "change-id", workflow.DefaultVersion, 1)
if v == workflow.DefaultVersion {
    // Old behavior
} else {
    // New behavior
}

Accessing Raw Temporal

You can access Temporal primitives when needed:

// Access Temporal client
builder := core.NewWorker().WithConfig(config).WithFlow(flow)
builder.Build()
client := builder.Client()

// Start workflows programmatically
run, err := client.ExecuteWorkflow(ctx, options, "workflow-name", input)

// Query workflow
err := client.QueryWorkflow(ctx, workflowID, "", "query-name", &result)

// Signal workflow
err := client.SignalWorkflow(ctx, workflowID, "", "signal-name", data)

Temporal UI Integration

All Resolute workflows appear in the Temporal Web UI:

  • Workflow List - See all running/completed flows
  • Workflow Detail - View execution history, inputs/outputs
  • Activity Details - Inspect individual node executions
  • Search - Query by workflow type, status, time range

Access at http://localhost:8233 (local) or your Temporal Cloud dashboard.

Best Practices

1. Leverage Durable Execution

Design flows to benefit from durability:

// Good: Each step is recoverable
flow := core.NewFlow("robust").
    Then(fetchNode).   // If crash here, fetched data is saved
    Then(processNode). // Restarts from process, not fetch
    Then(storeNode).
    Build()

2. Use Appropriate Timeouts

Match timeouts to expected activity duration:

// External API call - may be slow
fetchNode := core.NewNode("fetch", fetch, input).
    WithTimeout(5 * time.Minute)

// Local processing - should be fast
processNode := core.NewNode("process", process, input).
    WithTimeout(30 * time.Second)

3. Configure Retries for Transient Failures

External systems may have temporary issues:

apiNode := core.NewNode("api-call", callAPI, input).
    WithRetry(core.RetryPolicy{
        InitialInterval:    time.Second,
        BackoffCoefficient: 2.0,
        MaximumAttempts:    5,
    })

4. Monitor via Temporal UI

Regularly check:

  • Failed workflow rate
  • Activity retry rate
  • Queue latency
  • Worker health

Resources

See Also

  • Flows - How flows map to workflows
  • Nodes - How nodes map to activities
  • Workers - How workers connect to Temporal
  • Deployment - Production deployment