import type { FormField, FormFieldValues } from "@automate.ax/catalog"
import type { Encodable } from "@automate.ax/codec"
import type {
HttpRequestBody,
HttpRequestBodyType,
} from "@automate.ax/codec/http"
import type { StandardSchemaV1 } from "@standard-schema/spec"
import {
type DashboardRunConfig,
dashboardRunTriggerDefinition,
} from "@automate.ax/catalog/triggers/core-dashboard"
import {
getHttpTriggerEndpoint,
httpRequestTriggerDefinition,
type HttpTriggerScope,
} from "@automate.ax/catalog/triggers/core-http"
import { automationInvokedTriggerDefinition } from "@automate.ax/catalog/triggers/core-invocation"
import {
getMailhookAddress,
mailhookEmailReceivedTriggerDefinition,
type MailhookScope,
} from "@automate.ax/catalog/triggers/core-mailhook"
import { cronTickTriggerDefinition } from "@automate.ax/catalog/triggers/core-schedule"
import {
getAutomationInvocationEnvironment,
getAutomationPlanningContext,
getNextHookLocation,
} from "../../automation/runtime"
import { subscribe } from "../../automation/subscribe"
import {
type Signal,
transform,
withSignalStaticProperties,
} from "../../automation/signal-protocol"
import { subscriptionEventId } from "../../automation/signal-resolution"
import {
AUTOMATION_INVOCATION_EVENT_TYPE,
userInvocationEntrypointSchema,
} from "./invocation"
import { decodeHttpRequestEvent, type HttpRequestPayload } from "./http-request"
export interface ScheduleConfig {
/** Five-field cron expression describing the schedule. */
schedule: string
/** IANA time zone used to evaluate the schedule. Defaults to UTC. */
timeZone?: string
}
export type ScheduleEvent = {
/** Exact scheduled time represented by this event. */
scheduledAt: Date
}
export type HttpRequestMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE"
type HttpRequestSubscriptionConfig =
| {
/** Require a signed URL created by an action in this automation. */
protection: "callback"
/** Protected callbacks always use their unique trigger endpoint. */
scope?: "trigger"
waitForResponse?: boolean
}
| {
protection?: never
/** Scope at which HTTP requests are received. Defaults to this trigger. */
scope?: HttpTriggerScope
waitForResponse?: boolean
}
export type HttpRequestConfig<
TBodySchema extends StandardSchemaV1 | undefined = undefined,
> = HttpRequestSubscriptionConfig &
(TBodySchema extends StandardSchemaV1
? {
/** Standard Schema validator applied to the decoded request body. */
body: TBodySchema
}
: { body?: never })
export type HttpRequestEvent
= {
/** Request body decoded according to its content type. */
body: TBody
/** Representation selected from the request's content type. */
bodyType: HttpRequestBodyType
headers: Record
method: HttpRequestMethod
path: string
query: Record
/** Exact request bytes, preserved for signature verification. */
rawBody: Uint8Array
/** Stable identity passed to and emitted by `respondToHttpRequest`. */
requestId: string
url: string
}
export type HttpRequestTrigger = Signal<
HttpRequestEvent
> & {
/** Public URL that receives requests for this trigger. */
readonly endpoint: string
}
export type {
HttpRequestBody,
HttpRequestBodyType,
HttpRequestForm,
HttpRequestFormValue,
} from "@automate.ax/codec/http"
export interface MailhookConfig {
/** Scope at which the inbound address is shared. Defaults to this trigger. */
scope?: MailhookScope
}
export interface MailhookEvent {
/** Other To, Cc, and Bcc recipients included on the message. */
additionalRecipients: string[]
/** Attached files, including inline MIME parts. */
attachments: File[]
/** Original message headers normalized to lowercase names. */
headers: Record
/** HTML message body, when present. */
html: string | null
/**
* RFC 5322 Message-ID of this message's parent, from its In-Reply-To header.
* Use `messageId`, not this value, when replying to this received message.
*/
inReplyTo: string | null
/** Whether the message has an In-Reply-To header. */
isReply: boolean
/**
* On-the-wire RFC 5322 Message-ID supplied by the sender. Use this value as
* In-Reply-To when replying to this received message.
*/
messageId: string
/** Routing suffix after `+` in the recipient address. */
plusPath: string | null
/** When Resend received the message. */
receivedAt: Date
/**
* On-the-wire RFC 5322 Message-IDs in the conversation ancestry. Append this
* message's `messageId` when building a reply's References header. When this
* is empty, start with `inReplyTo` if present.
*/
references: string[]
/** Sender address, when supplied. */
senderAddress: string | null
/** Sender display name, when supplied. */
senderName: string | null
/** Message subject. */
subject: string
/** Plain-text message body, when present. */
text: string | null
}
export type MailhookTrigger = Signal & {
/** Unique inbound address for this mailhook scope. */
readonly address: string
}
export type DashboardRunValues =
FormFieldValues
export interface DashboardRunUser {
/** Authenticated member's email address. */
email: string
/** Authenticated member's user ID. */
id: string
/** Authenticated member's display name. */
name: string
}
export interface DashboardRunEvent {
/** Values collected from the configured fields. */
data: DashboardRunValues
/** Time at which Automate.ax started the run. */
triggeredAt: Date
/** Organization member who started the run. */
triggeredBy: DashboardRunUser
}
export type DashboardRunTrigger = Signal<
DashboardRunEvent
>
export type { DashboardRunConfig } from "@automate.ax/catalog/triggers/core-dashboard"
export type { FormField } from "@automate.ax/catalog"
/**
* Subscribes the current automation to a recurring cron schedule.
*
* @param config - Cron expression and optional IANA time zone.
*/
export function onSchedule(config: ScheduleConfig) {
return subscribe(cronTickTriggerDefinition, config)
}
/**
* Subscribes the current automation to HTTP requests.
*
* @param config - HTTP request subscription options.
* @throws When declared outside an automation invocation.
*/
export function onHttpRequest(
config: HttpRequestConfig,
): HttpRequestTrigger>
/** @inheritdoc */
export function onHttpRequest(config?: HttpRequestConfig): HttpRequestTrigger
export function onHttpRequest(
config: HttpRequestSubscriptionConfig & { body?: StandardSchemaV1 } = {},
): HttpRequestTrigger {
const environment = getAutomationInvocationEnvironment()
const location = getNextHookLocation()
if (!environment || !location) {
throw new Error(
"HTTP triggers must be declared inside an automation invocation.",
)
}
const { body: bodySchema, ...subscriptionConfig } = config
const request = subscribe(
httpRequestTriggerDefinition,
subscriptionConfig,
{
endpoint: getHttpTriggerEndpoint({
...environment,
hookSlot: location.slot,
scope: subscriptionConfig.scope ?? "trigger",
scopePath: location.scopePath,
}),
},
)
// Keep decoding separate so request addressing remains independent of body decoding.
const decoded = transform(request, (event) =>
decodeHttpRequestEvent(event, bodySchema),
)
const requestId = subscriptionEventId(request)
return withSignalStaticProperties(
transform([decoded, requestId], (event, resolvedRequestId) => ({
...event,
requestId: resolvedRequestId,
})),
{ endpoint: request.endpoint, requestId },
)
}
/**
* Subscribes the current automation to email received at a unique address.
*
* Append `+suffix` before the `@` to receive the suffix as `plusPath`. Use the
* trigger's address as `sendEmail({ replyTo })` to route replies back here.
*
* @param config - Mailhook address scope.
* @throws When declared outside an automation invocation.
*/
export function onMailhook(config: MailhookConfig = {}): MailhookTrigger {
const environment = getAutomationInvocationEnvironment()
const location = getNextHookLocation()
if (!environment || !location) {
throw new Error(
"Mailhook triggers must be declared inside an automation invocation.",
)
}
return subscribe(
mailhookEmailReceivedTriggerDefinition,
config,
{
address: getMailhookAddress({
...environment,
hookSlot: location.slot,
scope: config.scope ?? "trigger",
scopePath: location.scopePath,
}),
},
)
}
/**
* Adds an authenticated dashboard run form to the current automation.
*
* Only authenticated members of the owning organization can submit the run from
* the Automations page.
*
* @param config - Form content and ordered field definitions.
* @throws When declared outside an automation invocation.
*/
export function onDashboardRun(
config: DashboardRunConfig,
): DashboardRunTrigger {
if (!getNextHookLocation()) {
throw new Error(
"Dashboard run triggers must be declared inside an automation invocation.",
)
}
return subscribe>(
dashboardRunTriggerDefinition,
config,
)
}
/**
* Subscribes the current automation to typed programmatic invocations.
*
* The type argument is compile-time only; invocation payloads accept any value
* supported by the Automate.ax codec. Omit the entrypoint to use `"default"`.
* Entrypoints beginning with `__$` are reserved by Automate.ax.
*
* @param entrypoint - Static name used to route invocations to this trigger.
* @throws When the entrypoint is invalid, reserved, or already defined.
*/
export function onInvocation(entrypoint?: string) {
const normalizedEntrypoint = userInvocationEntrypointSchema.parse(entrypoint)
if (
getAutomationPlanningContext()?.subscriptions.some(
(subscription) =>
subscription.eventType === AUTOMATION_INVOCATION_EVENT_TYPE &&
typeof subscription.config === "object" &&
subscription.config !== null &&
"entrypoint" in subscription.config &&
subscription.config.entrypoint === normalizedEntrypoint,
)
) {
throw new Error(
`Invocation entrypoint ${JSON.stringify(normalizedEntrypoint)} is already defined by this automation.`,
)
}
return subscribe(automationInvokedTriggerDefinition, {
entrypoint: normalizedEntrypoint,
})
}