Skip to content
Maple Docs
Open app
Browse the docs
On this page

Alchemy resources

Every Maple resource the Alchemy provider manages: props, outputs and lifecycle for dashboards, alert rules, alert destinations, API keys and ingest keys.

The resources @maple-dev/alchemy manages. New here? Start with the Alchemy quick start.

ResourceLifecycleMain output
Maple.DashboardCreate, update in place, deletedashboardId
Maple.AlertDestinationCreate, update in place, delete. Changing type replaces itdestinationId
Maple.AlertRuleCreate, update in place, delete. Ownership-tagged per stackruleId
Maple.ApiKeyCreate, roll, revoke. Any other prop change replaces the keysecret
Maple.IngestKeysRead-only singletonprivateKey

Props use the API’s snake_case wire shape, exactly as the interactive API reference documents the matching create endpoint. Outputs use camelCase.

Maple.Dashboard

Effect.gen(function* () {
	const dash = yield* Maple.Dashboard("checkout", {
		name: "Checkout",
		description: "Golden signals for the checkout flow",
		tags: ["checkout"],
		time_range: { type: "relative", value: "12h" },
		widgets: [/* widget documents */],
		sections: [/* collapsible widget groups */],
		variables: [/* dashboard variables */],
	})
})
PropTypeNotes
namestringRequired
descriptionstring | null
tagsstring[]
time_rangeobjecte.g. { type: "relative", value: "12h" }
widgetsobject[]Passed through verbatim
sectionsobject[]Widgets join one with section_id and tab_id; their layout is relative to the section’s grid
variablesobject[]Passed through verbatim

Outputs: dashboardId (dash_…), name.

The easiest way to get a widget document is to build the dashboard in the UI and copy it from GET /v2/dashboards/{id}. See Build dashboards.

Maple.AlertDestination

A notification channel that alert rules deliver to. Props are discriminated on type:

typeChannel fields
pagerdutyintegration_key
webhookurl, optional signing_secret
discordwebhook_url
telegrambot_token, chat_id
emailmember_user_ids (Maple user IDs in your org)

Every variant also takes name (required) and enabled (defaults to true on both create and update).

Effect.gen(function* () {
	const hooks = yield* Maple.AlertDestination("incident-webhook", {
		type: "webhook",
		name: "Incident bot",
		url: "https://hooks.example.com/maple",
		signing_secret: Redacted.make(process.env.WEBHOOK_SECRET!),
	})
})
  • Channel secrets (integration_key, signing_secret, webhook_url, bot_token) are write-only: the API never returns them. Pass them as Redacted values so they stay out of logs and plan output. Because they cannot be read back, the provider re-sends them on every update.
  • Changing type replaces the destination with a new ID.
  • Slack, other chat-connector and Hazel destinations depend on integrations linked in Maple, so manage them in Notification destinations, not here.
  • Deleting a destination that rules still reference fails with a conflict. When the rules live in the same stack, Alchemy deletes them first.

Outputs: destinationId (dest_…), name, type, enabled.

Maple.AlertRule

Effect.gen(function* () {
	yield* Maple.AlertRule("api-p95", {
		name: "API p95 latency",
		severity: "warning",
		signal_type: "p95_latency",
		comparator: "gt",
		threshold: 800,
		window_minutes: 10,
		service_names: ["api"],
		consecutive_breaches_required: 2,
		destination_ids: [oncall.destinationId],
	})
})
PropTypeNotes
namestringRequired. Unique per organization
severity"warning" | "critical"Required
signal_typeAlertSignalTypeRequired. See values below
comparatorAlertComparatorRequired. See values below
thresholdnumberRequired. Error rates are 0-1 ratios
threshold_uppernumber | nullUpper bound for between / not_between
window_minutesnumberRequired
destination_idsstring[]Required. dest_… IDs, usually destination.destinationId outputs
enabledboolean
notesstring | null
service_namesstring[]Only these services
exclude_service_namesstring[]All services except these
group_bystring[] | null
tagsstring[]Up to 19. alchemy: tags are reserved (see below)
minimum_sample_countnumber
consecutive_breaches_requirednumberBreaching windows before it fires
consecutive_healthy_requirednumberHealthy windows before it resolves
renotify_interval_minutesnumber
alert_on_no_databooleanCount a window with no data as a breach
apdex_threshold_msnumber | nullFor apdex rules
query_builder_draftobject | nullFor builder_query rules, passed through verbatim
raw_query_sqlstring | nullFor raw_query rules
raw_query_reducerstring | nullidentity, sum, avg, min or max
notification_templateobject | null
  • signal_type: error_rate, p95_latency, p99_latency, apdex, throughput, builder_query, raw_query
  • comparator: gt, gte, lt, lte, eq, neq, between, not_between

Signal-specific fields are validated by the API against signal_type, so a mismatch fails the deploy with a 400. See Alert rules for what each signal measures.

Outputs: ruleId (alrt_…), name, enabled.

Ownership

Each rule carries one alchemy:<hash> tag that identifies the stack, stage and logical resource that owns it. It uses one of Maple’s 20 tag slots, which is why tags is capped at 19. The tag is what makes state recovery safe:

  • If Alchemy state is lost, the provider finds the rule by name and takes it back only when the ownership tag matches.
  • A rule with the same name and no matching tag (created in the UI, or by another stack or stage) fails the deploy instead of being overwritten. To take it over deliberately, wrap the resource in Alchemy’s adopt(true) or deploy with --adopt.
  • A stack never deletes a rule another stack has adopted.

Keep the ownership tag intact, and use distinct rule names for different stages that share one Maple organization.

Maple.ApiKey

Effect.gen(function* () {
	const ci = yield* Maple.ApiKey("ci", {
		name: "ci-pipeline",
		scopes: ["dashboards:write", "alerts:read"],
		rotate: 1,
	})
	// ci.secret is a Redacted<string>; bind it into another resource's env.
})
PropTypeNotes
namestringRequired
descriptionstring
scopesstring[]Omit for full access
kind"standard" | "mcp"Defaults to standard. mcp keys only work on the MCP server
expires_in_secondsnumber
rotatenumber | stringBump it (1 to 2) to roll the key

The API has no update endpoint for keys:

  • Changing name, description, scopes, kind or expires_in_seconds replaces the key: a new ID and a new secret, and the old key is revoked.
  • Bumping rotate rolls the key in place: same ID, name and scopes, new secret.
  • Deleting the resource revokes the key.

Outputs: keyId (key_…), name, keyPrefix, secret (Redacted, the full maple_ak_… value).

The API returns a secret only once, at create or roll time. The provider stores it in Alchemy state, which is the only place it survives, so keep your state somewhere durable and encrypted.

Maple.IngestKeys

Effect.gen(function* () {
	const ingest = yield* Maple.IngestKeys("ingest")
	// ingest.publicKey  -> maple_pk_… for browser and mobile senders
	// ingest.privateKey -> maple_sk_… for server-side senders
})

A read-only view of your organization’s ingest keys. It takes no props, and there is one per organization. Deleting the resource only stops tracking it; the keys stay valid. Rolling is not modeled, because a roll immediately invalidates the old key for every sender. Roll ingest keys from Settings → Ingestion instead.

Outputs: publicKey, privateKey (both Redacted), publicRotatedAt, privateRotatedAt.

Drift and sync

  • A deploy compares declared props against what the API returns and patches only when they differ. Fields you leave out of props stay unmanaged, so edits to them in the UI are kept.
  • alchemy sync restores declared dashboard fields and alert rule configuration to what the stack declares.
  • Alert destination secrets cannot be compared, so drift in them is only caught when you change the prop.

Errors and retries

The client retries 429 and 5xx responses with bounded exponential backoff and honors retry_after_seconds from the error envelope. The API allows 600 requests per 60 seconds per key, which a large stack stays well under.

Failures surface as typed errors in Effect’s error channel. An API failure is tagged with the server’s own @maple/http/… error tag and carries the HTTP status and the error body (type, code, message); Maple.MapleErrorTags has the tags to catchTag on. Client-side failures are MapleApiTransportError, MapleApiResponseDecodeError, MapleApiProtocolError and MapleApiRequestEncodingError. Rule ownership conflicts are MapleAlertRuleOwnershipError, and invalid or reserved rule tags are MapleAlertRuleTagsError.