Skip to main content

Runtime Execution

Qefro Runtime is the execution engine for all Marketplace App flows. It interprets declarative YAML — entities, workflows, triggers — and executes them through a unified flow engine. Apps define what the business logic is; the Runtime defines how it runs.

Marketplace App (metadata)
│
├── manifest.yaml
├── entities/
├── workflows/
└── ui/
│
▼
Qefro Runtime
│
┌─────┼──────┐
▼ ▼ ▼
FlowRunner EntityService Event Bus

Architecture principle​

Marketplace Apps define WHAT the business application is. Qefro Runtime defines HOW the metadata is executed.

Apps contain no executable code. Every behavior — data validation, storage, flow execution, event emission — is interpreted from metadata by the runtime.

Flow catalog​

The flow catalog is the runtime's registry of all available flows in a workspace. It merges flows from two sources:

  1. Installed Marketplace flows — compiled from solution YAML at install time
  2. SDK-synced flows — advertised by external SDK connectors

Flow matching​

When a user sends a message, the runtime:

  1. Embeds the message via an embedding model
  2. Scores all catalog flows using cosine similarity against each flow's declared intent utterances
  3. Selects the single best match if its score exceeds the threshold (≥ 0.8)
  4. Starts a new flow execution or resumes an existing one

Intent utterances​

Flows declare example utterances in the manifest's triggers block:

triggers:
- id: request_viewing
workflow: request-viewing
match:
intents:
- schedule a viewing
- book a viewing
- arrange a viewing
- view the property

The runtime uses these utterances for semantic matching. More diverse utterances improve matching accuracy.

FlowRunner state machine​

The FlowRunner executes one flow step at a time in a loop. Each step produces an outcome that determines what happens next.

Step outcomes​

OutcomeMeaningNext action
AdvanceStep completed, move to nextExecute next step in sequence
Goto(target)Conditional branchJump to named step
WaitInputAwaiting user textSuspend execution, send prompt
WaitUploadAwaiting file uploadSuspend execution, send prompt
WaitApprovalAwaiting portal approvalSuspend execution
WaitChallengeAwaiting identity verificationSuspend execution
Pause(wake_at)Timed delaySuspend until wake time
CompleteTerminal successEnd execution
Fail(reason)Terminal failureEnd execution with error

Safety bound​

The FlowRunner enforces a maximum of 50 steps per turn to prevent infinite loops. If a flow exceeds this limit, it fails with a clear error message.

Execution lifecycle​

User message
↓
Flow matching (catalog)
↓
Flow selected
↓
Variable seeding (context + memory)
↓
Step loop:
├── Load step definition
├── Emit StepStarted event
├── Execute step (enter_step)
├── Handle outcome
│ ├── Advance → next step
│ ├── WaitInput → suspend, send prompt
│ ├── Complete → persist, emit events
│ └── Fail → persist error
└── Repeat until terminal

Entity operation dialect​

Marketplace App flows use a declarative dialect for entity operations. The runtime translates these into storage operations via EntityService.

Operation format​

entity.<entity_name>.<operation>
OperationStorage mappingDescription
entity.property.liststorage.findList records with optional filters
entity.property.getstorage.getGet a single record by code or UUID
entity.property.createstorage.insertCreate a new record
entity.property.updatestorage.updateUpdate an existing record
entity.property.deletestorage.deleteSoft-delete a record

Execution modes​

Modeexecution: valueAdapterDescription
RuntimeruntimeRuntimeAdapterEntityService — metadata-only, no external calls
HTTPhttpConnectorBridgeProxied to external API via connection

Marketplace Apps (hosting: runtime) always use execution: runtime. SDK connectors use execution: http with tool names like search_products.

Reference resolution​

For get, update, and delete operations, the runtime accepts either a UUID or a human-readable code:

# Both work:
input_map:
id: id # user provides "VW-1001" or a UUID

The runtime resolves codes to UUIDs by searching the entity's code field. If zero or multiple records match, the operation fails.

Variable seeding​

When a flow starts, the runtime seeds its variable context from multiple sources:

1. Explicit user input (extracted from current message)
↓
2. Current flow state (active execution's variables)
↓
3. Conversation memory (values from earlier messages)
↓
4. Customer profile (person_id, phone, email from Customer Hub)
↓
5. Knowledge base (documents, FAQs — for RAG flows)
↓
6. Ask the user (only when every other source is empty)

Conversation state​

The runtime maintains a ConversationState for each conversation:

ConversationState
├── conversation_id
├── person_id (Customer Hub Person UUID)
├── channel_subject (WhatsApp E.164, widget ID)
├── active_flow (currently executing flow id)
├── values (collected key-value pairs)
├── entities (tracked entity references)
├── customer (phone, email, preferences)
├── knowledge (documents, FAQs, policies)
├── authenticated (bool)
└── working (post-flow working context)

Memory interaction​

The runtime loads durable memory from Postgres at the start of each turn. Memory includes:

  • Person identity: Name, email, phone from Customer Hub
  • Collected values: Variables from previous flows in this conversation
  • Entity mentions: Records referenced in the conversation
  • Knowledge: Retrieved documents and FAQs

Memory values seed the flow's variable context, allowing ask steps to auto-skip when a value is already known.

Ask step auto-skip​

An ask step is skipped when:

  1. The flow's variable context already has a usable value for the requested field, AND
  2. The value was explicitly collected (not just inferred from memory)

This prevents the flow from re-asking for information the user already provided, while still confirming ambiguous or inferred values.

Runtime context​

Every flow execution runs within a context that includes:

Authenticated identity​

FieldSourceTrust
tenant_idPlatform authenticationTrusted
workspace_idInstallation contextTrusted
installation_idSolution installationTrusted
person_idCustomer Hub PersonTrusted
user_idPortal user (if applicable)Trusted
session_idConversation sessionTrusted

Channel context​

FieldDescription
identity_channelWhatsapp, Widget, Portal, or Api
authentication_levelIdentity verification level
has_end_user_identityWhether the caller has verified identity

Permission context​

The runtime checks permissions before executing entity operations:

ChannelRBAC enforcedReason
WhatsAppNoCustomer Hub + surfaces handle access
WidgetNoCustomer Hub + surfaces handle access
PortalYesPortal users have roles and permissions
API (with portal user)YesPortal user RBAC applies
API (system/event)NoInternal flows, no user context

Event emission​

After every successful entity mutation, the runtime emits business events:

OperationEvents emitted
Create{entity}.created
Update{entity}.updated + status event (if status changed)
Delete{entity}.cancelled

Event payload​

Events include a CRM-compatible payload:

{
"data": { "id": "...", "code": "VW-1001", "property_title": "...", ... },
"current": { ... },
"previous": { ... },
"source": { "type": "marketplace_app", "id": "real-estate-pro-runtime" },
"workspace_id": "...",
"solution_id": "real-estate-pro-runtime",
"contact": { "id": "...", "name": "...", "phone": "...", "email": "..." },
"person_id": "..."
}

Idempotency​

Events use idempotency keys to prevent duplicates:

entity:{workspace_id}:{event_name}:{resource_id}

Multi-step conversations​

Flows can span multiple user messages. The runtime persists the flow's variable context between turns, allowing conversations like:

User: "I want to see properties"
Assistant: "What type of property?" ← ask step
User: "Apartment"
Assistant: "Which city?" ← ask step
User: "Madurai"
Assistant: "Here are matching properties:" ← tool + message steps
[property results]

Flow state persistence​

Between turns, the runtime stores:

  • Current step id
  • Collected variables
  • Execution status (WaitingForInput)
  • Associated conversation id

When the user replies, the runtime:

  1. Loads the active execution from Redis/Postgres
  2. Stores the user's reply in the variable context
  3. Resumes the step loop from the waiting step

Conversation context growth​

As a conversation grows, the context accumulates messages from all flows and general chat. This can dilute the semantic matching for new flow starts. For best results:

  • Use fresh conversations (different phone numbers) for testing different flows
  • Keep flow intent utterances distinct from each other
  • Use specific utterances rather than generic ones

Trusted metadata vs user input​

The runtime enforces a strict trust boundary:

Trusted (cannot be overridden)​

SourceExamples
Runtime contexttenant_id, workspace_id, installation_id
Customer identityperson_id (from Customer Hub)
Flow constantsValues in constants: block
Signed metadataEntity definitions, flow definitions

Untrusted (user/LLM input)​

SourceExamples
Ask stepsproperty_title, date, amount
Extracted entitiesNames, dates, numbers from messages
Conversation memoryPreviously collected values

Authority stripping​

Before executing any entity operation, the runtime strips authority-bearing fields from caller-supplied parameters:

  • workspace_id
  • tenant_id
  • installation_id
  • organization_id
  • customer_id
  • person_id
  • context

Identity comes from the authenticated context only. Even if the LLM generates a person_id value, it is discarded.

Marketplace Apps vs SDK connectors​

AspectMarketplace AppSDK Connector
Hostingruntimeexternal
ExecutionRuntimeAdapter (EntityService)SDKAdapter (/qefro tools)
StoragePlatform-managedApp-managed (ctx.storage)
Toolsentity.<name>.<op>HTTP tool names
CodeYAML onlyNode.js / Python / Rust
DeploymentNo server requiredSelf-hosted by developer

Both use the same FlowRunner engine. The difference is the adapter that executes tool steps.