Adobe I/O Runtime actions for the Vitamix storefront. Handles form submissions (product registration, newsletter signup, checkout-related EBS calls) and automated order synchronization to Oracle EBS.
Public HTTP endpoint that receives form submissions from the storefront. Validates the payload, publishes a form.submitted I/O Events CloudEvent for async processing, and for certain forms (e.g. product registration) calls EBS directly.
Event-driven action triggered by form.submitted event (concurrency=1). Reads the destination sheet, appends the submission, and writes it back. Sends email notifications where configured.
Returns the currently deployed version ({ "version": "..." }). Used by post-deploy smoke tests.
Synchronizes completed commerce orders to Oracle EBS. Runs on a 5-minute cron schedule and is also available as an HTTP endpoint for status checks and manual triggers.
Scheduled mode (every 5 min): Reads the global orders journal since the last cursor, filters to terminal events (payment_completed / payment_cancelled), and for each completed order builds a SOAP XML payload and POSTs it to EBS via a static-IP proxy. Cancelled orders are skipped. A 15-minute overlap is applied to journal queries to catch late-arriving entries.
Supported payment methods: Credit card (Chase), PayPal, Apple Pay (Chase wallet), Affirm.
Emails a daily digest of newly published recipes to a configured list. Runs on a daily cron and is also available as an HTTP endpoint for status checks and manual triggers.
Production only. Stage deploys the same manifest, so the scheduled run is
gated by RECIPE_NOTIFY_ENABLED — it is set to "true" only in the prod deploy
env and the action no-ops immediately otherwise. This prevents duplicate emails
to the shared recipient list (state is per-namespace, but the DA template and its
recipients are shared).
Scheduled mode (daily): Polls the CalcMenu GetUpdatedRecipes journal since
a persisted timestamp cursor. The API input is date-granular but responses carry
full-precision DateUpdated timestamps and a Status (New/Updated/
Deleted), so the action keeps recipes whose DateUpdated is strictly newer
than the cursor (millisecond compare — dedupes the re-fetched boundary day),
filters to Status="New", resolves each recipe's deep link from the published
query-index.json (by Number), and sends the digest. The cursor advances to
the batch's max DateUpdated only on a successful run. The first run cold-starts
(seeds the cursor without emailing) to avoid blasting the back-catalogue.
Recipients + copy: authored in a DA document at
/config/recipes/digest-template (same format as email-template.html, with a
{{digest}} placeholder where the recipe table is injected). No recipient env
var.
POST /forms/submit
Content-Type: application/json
{ "formId": "product-registration", "data": { ... } }
GET /ebs-sync/ebs-sync
Authorization: Bearer <SYNC_STATUS_TOKEN>
Returns JSON with current sync state: since, lastProcessedOrderId, lastRun, status, processedCount, failedCount, lastError.
POST /ebs-sync/ebs-sync
Authorization: Bearer <SYNC_STATUS_TOKEN>
Content-Type: application/json
{
"since": "<ISO 8601>",
"until": "<ISO 8601>", // optional upper bound
"duration": <minutes> // optional, alternative to "until"
}
Triggers a sync run starting from the given timestamp. since is required. Provide until or duration (not both) to cap the query window; omit both to scan up to the current time. The cursor advances normally after a successful run.
GET /recipe-notify/recipe-notify
Authorization: Bearer <RECIPE_NOTIFY_TOKEN>
Returns JSON: since, lastRun, status, processedCount, lastError.
POST /recipe-notify/recipe-notify
Authorization: Bearer <RECIPE_NOTIFY_TOKEN>
Content-Type: application/json
{
"since": "<ISO 8601>", // optional cursor override (backfill)
"dryRun": true // optional — compute new recipes + links, do not send
}
dryRun bypasses the prod gate and never sends or advances the cursor, so stage
can be exercised on demand. It returns the computed new-recipe set with resolved
deep links.
Deployed via GitHub Actions using aio app deploy and semantic-release.
| Environment | Trigger | Workflow |
|---|---|---|
| Stage | PR opened/updated against main |
.github/workflows/deploy_stage.yml |
| Production | Push to main |
.github/workflows/deploy_prod.yml |
Production deployments use semantic-release to version and tag releases automatically.
| Variable | Description |
|---|---|
ORG |
Organization slug (hardcoded: aemsites) |
SITE |
Site slug (hardcoded: vitamix) |
LOG_LEVEL |
Logging level (default: info) |
AIO_CLIENTID |
Adobe I/O client ID |
AIO_CLIENTSECRET |
Adobe I/O client secret |
AIO_SCOPES |
Adobe I/O OAuth scopes |
AIO_IMSORGID |
Adobe IMS org ID |
AIO_EVENTS_PROVIDER_ID |
I/O Events provider ID |
EMAIL_TOKEN |
Token for sending notification emails |
PROXY_TOKEN |
Bearer token for the static-IP proxy (AWS API Gateway) |
EBS_BASE_URL |
Production EBS SOAP endpoint |
EBS_BASE_URL_STAGE |
Stage EBS SOAP endpoint |
EBS_API_KEY |
Production EBS API key |
EBS_API_KEY_STAGE |
Stage EBS API key |
NEWSLETTER_BASE_URL |
Production newsletter API base |
NEWSLETTER_BASE_URL_STAGE |
Stage newsletter API base |
NEWSLETTER_API_KEY |
Production newsletter API key |
NEWSLETTER_API_KEY_STAGE |
Stage newsletter API key |
| Variable | Description |
|---|---|
ORG |
Organization slug (hardcoded: aemsites) |
SITE |
Site slug (hardcoded: vitamix) |
LOG_LEVEL |
Logging level |
SYNC_STATUS_TOKEN |
Bearer token for the status/trigger HTTP APIs |
EDGE_COMMERCE_API_BASE |
Edge Commerce API base URL |
EDGE_COMMERCE_API_ORDERS_TOKEN |
Bearer token for the orders/journal API |
EBS_BASE_URL |
Production EBS SOAP endpoint |
EBS_BASE_URL_STAGE |
Stage EBS SOAP endpoint |
EBS_API_KEY |
Production EBS API key |
EBS_API_KEY_STAGE |
Stage EBS API key |
PROXY_TOKEN |
Bearer token for the static-IP proxy |
All RECIPE_* value vars have in-code defaults (see sync.js); override only if they change.
| Variable | Description | Default |
|---|---|---|
ORG / SITE |
Org/site slug | aemsites / vitamix |
LOG_LEVEL |
Logging level | info |
EMAIL_TOKEN |
Productbus email service token (reused from forms) | — |
AIO_CLIENTID / AIO_CLIENTSECRET / AIO_SCOPES |
S2S creds to mint the IMS token for reading the DA template (reused from forms) | — |
RECIPE_API_URL |
CalcMenu GetUpdatedRecipes base URL |
https://vitamix.calcmenuweb.com/ws/service.asmx/GetUpdatedRecipes |
RECIPE_API_ID |
CalcMenu API user id | API |
RECIPE_API_PSWD |
CalcMenu API password | Vitamix! |
RECIPE_SITE_BASE |
Deep-link site base | https://www.vitamix.com |
RECIPE_LINK_LOCALE |
Deep-link locale path | us/en_us |
RECIPE_DIGEST_TEMPLATE |
DA path of the digest template | /config/recipes/digest-template |
RECIPE_NOTIFY_TOKEN |
Bearer token for the status/trigger HTTP APIs (HTTP access denied if unset) | — |
RECIPE_NOTIFY_ENABLED |
Prod-only gate — scheduled run sends only when "true". Set in prod deploy env only. |
unset (no-op) |
- Create App Builder project
- Add service to project:
I/O Management API - Generate OAuth Server-to-Server credentials
- Set
.envvalues usingexample.envtemplate and values from Adobe Developer Console - Configure I/O Events:
- Create Event Provider — response
idbecomesAIO_EVENTS_PROVIDER_ID - Create Provider Metadata for
form.submittedevent - Register action handler for
forms/processoronform.submitted
- Create Event Provider — response
npm install
npm test
npm run build
npm run lintTests use Jest with ESM support (--experimental-vm-modules). The ebs-sync E2E tests compare generated SOAP XML against golden fixture files in test/fixtures/.