SDK Reference
Funnels and artifacts
Record the stages an artifact moves through with sendArtifactEvent, and tie AI generations to the artifact they produce.
A funnel is an ordered sequence of stages that something moves through. For each funnel, Otis tracks how far things get, where they stall, and what share of them succeed.
An artifact is a persistent thing that users create and return to across sessions, such as a document, a slide deck, a design, or a code file. An artifact funnel follows one artifact through stages such as create, edit and share. This page covers how to record those stages, and how to connect AI work to the artifact it produced.
Record a stage with sendArtifactEvent()
Call sendArtifactEvent() each time an artifact reaches a stage:
import { sendArtifactEvent } from "@runotis/sdk";
// The user creates the deck. This is the funnel's first stage.
sendArtifactEvent("doc-abc123", { stage: "create", type: "deck" });
// The user keeps working on it.
sendArtifactEvent("doc-abc123", { stage: "edit" });
// The user shares it. This is a success stage.
sendArtifactEvent("doc-abc123", { stage: "share", templateId: "t-42" });Required fields:
- Artifact ID, the first argument. A stable identifier for the artifact, such as
doc-abc123. Send the same ID for every stage of the same artifact. The SDK trims and lowercases the ID, and throws if the result contains any character other thana–z,0–9,_,.or-. When identifier hashing is on, the SDK hashes the ID before it leaves your app, as it does user and session IDs. stage, the stage this event records. The SDK applies the same trimming, lowercasing and character rules.
Optional fields:
type, the kind of artifact, such asdeckordoc. Otis uses it to compare kinds of artifact. The SDK applies the same trimming, lowercasing and character rules as for the ID, so a label such asPitch Deckthrows. Usepitch_deckinstead.- Any other key, stored on the event with an
artifact.prefix, sotemplateIdbecomesartifact.templateId. Use these keys for details about the event, such as a template ID or a word count.
An event sent inside a withContext() or wrap() scope picks up the user, session and group from that scope, so you don't pass identity to sendArtifactEvent().
Otis stores these properties without scanning them for personal data, as it does for custom event properties. To have a property that holds user-typed text scanned and redacted, mark its key as described in Custom events.
Match stage names to your funnel
Your project's funnel definition lists the stages in order and says which ones mean success and which mean failure. Otis sets up this definition with you when it instruments your code. The stage names you send must match it exactly, because a stage name the definition doesn't list isn't counted.
- First stage. Otis counts an artifact only once it has an event for the first stage, which is usually its creation.
- Success stages. One or more stages mean the artifact is finished, such as
shareorexport. - Failure stages. Stages such as
discardorabandonmean the user gave up on the artifact. If your product lets users discard an artifact, send that stage and include it in the definition. Otis can then tell an artifact the user discarded from one the user stopped working on. - Artifacts that run out of time. An artifact with no success or failure counts as timed out once it has sent no stage event for 14 days, by default. It also times out 14 days after its first-stage event, by default, even if it is still being edited. Only stage events count here. Other telemetry about the artifact doesn't.
Funnels describes how Otis counts progress and outcomes from these events.
Keep stage names stable once they ship. Events sent under a renamed stage no longer match the funnel definition, so Otis stops counting them.
Attribute AI work to an artifact with withArtifactContext()
Otis can report what each artifact cost to produce, and which AI responses led to an artifact being shared. To do this, the AI calls and follow-up events need to carry the artifact's ID. Wrap that work in withArtifactContext():
import { withArtifactContext, sendArtifactEvent, sendEvent } from "@runotis/sdk";
sendArtifactEvent(deckId, { stage: "create", type: "deck" });
await withArtifactContext(deckId, async () => {
const { streamText } = otis.wrap(ai);
await streamText({ model, prompt }); // carries the artifact ID
sendEvent("deck.preview_opened", { recipient }); // carries the artifact ID
});Everything recorded inside the scope carries the artifact ID: wrapped AI calls, traced() functions, sendEvent(), sendFeedbackSignal() and sendException().
withArtifactContext(artifactId, fn) is shorthand for otis.withContext({ artifactId }, fn). You can also set artifactId alongside other context fields:
await otis.withContext({ artifactId: deckId, userId, sessionId }, async () => {
await streamText({ model, prompt });
});You can pass an ID that the SDK has already hashed. The SDK recognizes it and doesn't hash it a second time.
Create the artifact ID before the AI call runs
Code often generates content first and saves the artifact afterward, with an ID that didn't exist while the model was running. By then the AI call has been recorded without the artifact ID, and Otis can't connect the two later. If your code creates the artifact record after generating, create the ID first and pass it to both.