How it works
Keep your business functions. Add reusable action controls.
Authorize the action. Verify the outcome. Confirm only what is supported.
Your application supplies identity and authorization policy, performs the booking, independently checks the result and sends the customer message. AnswerUp connects those functions to reusable execution and recovery controls.
A table for two. One missing response.
“Book a table for two at 7 pm.”
The restaurant books the table. Its response never reaches your agent, and the first lookup is unavailable. Should the agent try again? Can it say the booking is confirmed?
The next step is to check the original booking. A new booking could create a duplicate.
Same request. Same business functions.
What happens when the booking response is lost?
Without Core
You build the recovery controls.
- Your booking function sends the reservation.
- The response is lost; your lookup cannot establish the result.
- You maintain pending work, decide when to check again, prevent another booking and decide what the agent may say.
“I’m checking your booking. I can’t confirm it yet.”
Both approaches can be implemented correctly. Without Core, you build and maintain uncertainty tracking, verification, retry safety and claim controls.
With AnswerUp Core
Core provides the recovery controls.
- Adapt the same booking and lookup functions to the real provider and verifier contracts.
- AnswerUp returns
UNKNOWN; the host withholds a booking confirmation. - The host requests verifier-only reconciliation. Exact receipt evidence and permitted claims support the final message.
“Your table for two was verified.”
Your host still owns the business functions, authorization policy, durable storage and recovery scheduling. The verified local run made one booking, two lookups and zero redispatches.
Real code from the working example
Keep the functions. Add the contracts.
These excerpts are read directly from the executable source. The restaurant data is synthetic; AnswerUp Actions, Runtime and Core really execute.
- = Existing application code
- ~ Modified application code
- + New AnswerUp Core integration
Keep the booking call and receipt checks (blue). Adapt lookup arguments and provider/verifier returns (amber). Add Core registration, authorization, execution and reconciliation, including host bookkeeping (green). Reorganized or reformatted logic stays blue; lines mixing existing logic with an adapted return are amber.
Without Core
The application leaves a later lookup to its own recovery flow.
export async function withoutCore(restaurant, request) {
const binding = {
...request, tenantRef: 'tenant:demo',
effectIdentity: `legacy:${request.requestId}`,
};
try {
await restaurant.bookReservation(binding);
} catch {
try {
const receipt = await restaurant.lookupReservation(
binding.effectIdentity,
);
const exact = Object.keys(binding).every(
field => receipt[field] === binding[field],
);
if (exact && receipt.status === 'reserved') {
return 'Your table for two was verified.';
}
} catch {}
return 'The booking outcome needs a later check. Do not resend.';
}
return 'The booking response needs an independent check.';
}Source: without-core.mjs →With Core
Step 1: Adapt your existing business functions
async function dispatch({ input, context, operationIdentity, executionIdentity, effectIdentity, attemptIdentity }) {
const entry = admissions.get(input.requestId);
if (!entry || !sameInput(entry.input, input)) return { kind: 'PRECONDITION_FAILED' };
const binding = { ...input, tenantRef: context.tenant.tenantRef, operationIdentity, executionIdentity, effectIdentity, attemptIdentity };
entry.binding = binding;
bindings.set(effectIdentity, binding);
try {
await restaurant.bookReservation(binding);
return { kind: 'ACKNOWLEDGED' };
} catch {
return { kind: 'UNCERTAIN', reason: 'RESPONSE_LOST' };
}
}
async function verify({ input, context, effectIdentity }) {
const binding = bindings.get(effectIdentity);
if (!binding || binding.tenantRef !== context.tenant.tenantRef || !sameInput(binding, input)) {
return { kind: 'INDETERMINATE', observationRef: 'restaurant:unbound' };
}
try {
const receipt = await restaurant.lookupReservation(effectIdentity);
const exact = Object.keys(binding).every(field => receipt[field] === binding[field]);
if (exact && receipt.status === 'reserved') return { kind: 'PRESENT', observationRef: `restaurant:${effectIdentity}` };
} catch {}
return { kind: 'INDETERMINATE', observationRef: 'restaurant:unavailable' };
}The functions above connect to your business system. The additional code below shows how your application uses AnswerUp Core to govern execution and recovery.
Step 2: See how the application connects these functions to AnswerUp Core
submit(input) {
const prior = admissions.get(input.requestId);
if (prior) {
if (!sameInput(prior.input, input)) {
throw new Error('REQUEST_CONFLICT');
}
return prior.promise;
}
const entry = {
tenantRef, input: structuredClone(input),
claimPlan: structuredClone(claimPlan),
subject: restaurant.readSlot(input.slot)?.subject,
binding: null, promise: null,
};
admissions.set(input.requestId, entry);
entry.promise = runtime.run({
action, input,
context: {
...context,
delivery: {
deliveryId: `delivery:${input.requestId}`,
source: 'restaurant-lesson',
},
hostInvocation: {
invocationId: `${tenantRef}:${input.requestId}`,
},
},
}).then(result => ({
result, entry, message: confirmation(result, entry),
}));
return entry.promise;
},
async reconcile(initial) {
const result = await runtime.reconcile(
initial.result, { claimPlan },
);
return {
result, entry: initial.entry,
message: confirmation(result, initial.entry),
};
},submit and reconcile in the expandable host excerpt are methods in this example's local host. Its maps, trusted context and action registration are in the complete source. The SDK exports are defineProtectedAction and createProtectedActionRuntime; the calls to runtime.run and runtime.reconcile use their real contracts.
See the actual SDK action registration
The local host registers these adapters using defineProtectedAction and createProtectedActionRuntime. Validation, grounding and policy remain explicit.
const runtime = createProtectedActionRuntime();
runtime.register(defineProtectedAction({
name: action, version: '1',
validateProposal(input) {
return typeof input?.requestId === 'string' && /^[a-z0-9-]{1,40}$/.test(input.requestId)
&& typeof input.slot === 'string' && input.partySize === 2
? { valid: true, input: { requestId: input.requestId, slot: input.slot, partySize: input.partySize } }
: { valid: false, kind: 'INVALID_PROPOSAL', code: 'RESERVATION_INPUT' };
},
ground(input, trusted) {
const slot = restaurant.readSlot(input.slot);
if (!slot || slot.capacity < input.partySize) return { kind: 'UNRESOLVED', clarificationRequired: true, reason: 'SLOT_UNAVAILABLE', candidates: [] };
return { kind: 'GROUNDED', grounded: { subject: slot.subject }, scope: trusted.tenant.securityDomainRef,
subject: slot.subject, requestedEffect: { slot: input.slot, partySize: input.partySize, status: 'reserved' },
candidates: [{ referent: slot.subject, source: 'authoritative', attributes: { resource: slot.subject }, evidenceRefs: ['restaurant:synthetic-catalog'] }],
observationRef: 'restaurant:catalog-read' };
},
authorize: () => canBook ? 'PERMIT' : 'DENY',
provider: { adapterIdentity: 'restaurant-local-http-writer', dispatch },
verifier: { verifierIdentity: 'restaurant-exact-effect-reader', verify },
initialExecutionClaimPlan: claimPlan,
}));ProtectedActionDefinitionBase · ProviderDispatchResult · VerifierResult
What stays with your application?
You still own trusted identity, business policy, request admission, durable storage, authoritative read-back and the message sent to the customer. Core does not make an unavailable business system observable.
This small exercise demonstrates one process and one live reconciliation. The existing Generic HTTP reference adds a persisted single-writer host and explicit restart recovery.
Packages: Core 1.0.0-rc.1 · Runtime 0.2.0-preview.5 · Actions 0.2.0-preview.8. Based on Generic HTTP reference 0.2.0-preview.4.