> ## Documentation Index
> Fetch the complete documentation index at: https://smithers.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# <Poller>

> Poll an external condition with configurable backoff until satisfied or timed out.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
// Props
import { Poller } from "smithers-orchestrator";

type PollerProps = {
  id?: string; // default "poll"
  check: AgentLike | (() => Promise<unknown> | unknown);
  checkOutput: OutputTarget; // must include `satisfied: boolean`
  maxAttempts?: number; // default 30
  backoff?: "fixed" | "linear" | "exponential"; // default "fixed"
  intervalMs?: number; // delay BETWEEN attempts, default 5000
  checkTimeoutMs?: number; // timeout for one check attempt; unbounded when unset
  onTimeout?: "fail" | "return-last"; // default "fail"
  skipIf?: boolean;
  children?: ReactNode; // condition description
};
```

```tsx theme={"theme":{"light":"github-light","dark":"github-dark"}}
<Workflow name="wait-for-deploy">
  <Poller
    check={statusChecker}
    checkOutput={outputs.check}
    maxAttempts={20}
    intervalMs={10_000}
    backoff="exponential"
    onTimeout="fail"
  >
    Check whether the deployment to production has completed successfully.
  </Poller>
</Workflow>
```

## Notes

* `satisfied` drives the loop's `until`.
* The first attempt runs immediately; `intervalMs` is the delay *between* attempts.
* Backoff scales that gap. For the Nth gap (1-indexed): fixed = `intervalMs`;
  linear = `intervalMs * N`; exponential = `intervalMs * 2^(N-1)`.
* The gap is a durable [`<Timer>`](/components/timer): the run parks as
  `waiting-timer` between attempts and survives a crash or resume, rather than
  sleeping in-process. A detached run is resumed by the gateway's timer sweep.
* `intervalMs` does **not** bound how long a check may run. Use `checkTimeoutMs`
  to cap a single attempt.

## Source

The `<Poller>` implementation and the files it imports, straight from the package source. This section is generated; edit the source, not this block.

<CodeGroup>
  ```js Poller.js theme={"theme":{"light":"github-light","dark":"github-dark"}}
  import React from "react";
  import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
  import { SmithersContext } from "@smithers-orchestrator/react-reconciler/context";
  import { Task } from "./Task.js";
  import { Loop } from "./Ralph.js";
  import { Sequence } from "./Sequence.js";
  import { Timer } from "./Timer.js";
  /** @typedef {import("./PollerProps.ts").PollerProps} PollerProps */

  /**
   * Compute the real wall-clock delay (ms) for one gap between poll attempts.
   *
   * `gap` is 0-indexed over the gaps, not the attempts: gap 0 is the pause
   * between attempt 1 and attempt 2. Nothing precedes the first attempt, so the
   * first poll always fires immediately. The returned delay is enforced by a
   * durable <Timer>; it is not a task timeout.
   * @param {number} gap
   * @param {number} baseMs
   * @param {"fixed" | "linear" | "exponential"} strategy
   * @returns {number}
   */
  function computeDelayMs(gap, baseMs, strategy) {
      let raw;
      switch (strategy) {
          case "linear":
              raw = baseMs * (gap + 1);
              break;
          case "exponential":
              raw = baseMs * Math.pow(2, gap);
              break;
          case "fixed":
          default:
              raw = baseMs;
              break;
      }
      // A non-finite delay would be formatted as "Infinity ms"/"NaNms" (and any
      // value >= 1e21 as "1e+21ms"), none of which the engine's duration parser
      // accepts. Fail here, where the offending prop is still in scope.
      if (!Number.isFinite(raw)) {
          throw new SmithersError("INVALID_INPUT", `<Poller> computed a non-finite poll delay from intervalMs=${baseMs}, backoff="${strategy}".`);
      }
      return Math.max(0, Math.round(raw));
  }
  /**
   * @param {PollerProps} props
   */
  export function Poller(props) {
      if (props.skipIf)
          return null;
      const ctx = React.useContext(SmithersContext);
      const prefix = props.id ?? "poll";
      const maxAttempts = props.maxAttempts ?? 30;
      const backoff = props.backoff ?? "fixed";
      const baseInterval = props.intervalMs ?? 5000;
      const onTimeout = props.onTimeout ?? "fail";
      const iteration = ctx?.iterations?.[`${prefix}-loop`] ?? ctx?.iteration ?? 0;
      const checkRow = ctx?.outputMaybe(props.checkOutput, {
          nodeId: `${prefix}-check`,
          iteration,
      });
      const until = checkRow?.satisfied === true;
      // Determine if check is an agent or a compute function
      const isAgent = typeof props.check === "object" &&
          props.check !== null &&
          "generate" in props.check;
      // Build the check task
      const prompt = props.children ??
          "Check whether the condition is satisfied. Return an object with a satisfied boolean.";
      const checkTask = isAgent
          ? React.createElement(Task, {
              id: `${prefix}-check`,
              output: props.checkOutput,
              timeoutMs: props.checkTimeoutMs,
              agent: props.check,
              children: prompt,
          })
          : React.createElement(Task, {
              id: `${prefix}-check`,
              output: props.checkOutput,
              timeoutMs: props.checkTimeoutMs,
              children: props.check,
          });
      // Pace the loop with a durable <Timer> ahead of the check, inside a
      // <Sequence> so the scheduler holds the check back until the timer fires.
      // The timer is skipped on the first iteration (poll immediately), so the
      // delay before attempt N is the (N-1)th gap.
      const delayMs = iteration === 0 ? 0 : computeDelayMs(iteration - 1, baseInterval, backoff);
      return React.createElement(Loop, {
          id: `${prefix}-loop`,
          until,
          maxIterations: maxAttempts,
          onMaxReached: onTimeout === "fail" ? "fail" : "return-last",
      }, React.createElement(Sequence, null, React.createElement(Timer, {
          id: `${prefix}-delay`,
          duration: `${delayMs}ms`,
          skipIf: iteration === 0,
      }), checkTask));
  }
  ```

  ```js SmithersError.js theme={"theme":{"light":"github-light","dark":"github-dark"}}
  import { getSmithersErrorDocsUrl } from "./getSmithersErrorDocsUrl.js";
  /** @typedef {import("./SmithersErrorCode.ts").SmithersErrorCode} SmithersErrorCode */
  /** @typedef {import("./SmithersErrorOptions.ts").SmithersErrorOptions} SmithersErrorOptions */

  export class SmithersError extends Error {
      /** @type {SmithersErrorCode} */
      code;
      /** @type {string} */
      summary;
      /** @type {string} */
      docsUrl;
      /** @type {Record<string, unknown> | undefined} */
      details;
      /** @type {unknown} */
      cause;
      /** @type {string} */
      name;
      /**
     * @param {SmithersErrorCode} code
     * @param {string} summary
     * @param {Record<string, unknown>} [details]
     * @param {unknown | SmithersErrorOptions} [causeOrOptions]
     */
      constructor(code, summary, details, causeOrOptions) {
          const docsUrl = getSmithersErrorDocsUrl(code);
          // The 4th arg historically took a bare `cause`. An object counts as an
          // options bag only when it owns at least one known option key
          // (cause/includeDocsUrl/name), so most plain-object causes still
          // round-trip as the cause.
          const isOptionsObject = causeOrOptions &&
              typeof causeOrOptions === "object" &&
              !(causeOrOptions instanceof Error) &&
              (Object.prototype.hasOwnProperty.call(causeOrOptions, "cause") ||
                  Object.prototype.hasOwnProperty.call(causeOrOptions, "includeDocsUrl") ||
                  Object.prototype.hasOwnProperty.call(causeOrOptions, "name"));
          const options = /** @type {SmithersErrorOptions} */ (isOptionsObject
              ? causeOrOptions
              : { cause: causeOrOptions });
          // Append the docs pointer unless suppressed or the summary already contains it.
          const message = options.includeDocsUrl === false || summary.includes(docsUrl)
              ? summary
              : `${summary} See ${docsUrl}`;
          super(message, { cause: options.cause });
          Object.setPrototypeOf(this, new.target.prototype);
          this.name = options.name ?? "SmithersError";
          this.code = code;
          this.summary = summary;
          this.docsUrl = docsUrl;
          this.details = details;
          this.cause = options.cause;
      }
  }
  ```

  ```js context.js theme={"theme":{"light":"github-light","dark":"github-dark"}}
  // @smithers-type-exports-begin
  /** @typedef {import("./OutputSnapshot.ts").OutputSnapshot} OutputSnapshot */
  /** @typedef {import("./SmithersCtxOptions.ts").SmithersCtxOptions} SmithersCtxOptions */
  // @smithers-type-exports-end

  import React from "react";
  import { SmithersCtx } from "@smithers-orchestrator/driver/SmithersCtx";
  import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
  export { SmithersCtx } from "@smithers-orchestrator/driver/SmithersCtx";
  /** @type {React.Context<SmithersCtx<any> | null>} */
  export const SmithersContext = React.createContext(null);
  SmithersContext.displayName = "SmithersContext";
  /**
   * @template Schema
   * @returns {{ SmithersContext: React.Context<SmithersCtx<Schema> | null>, useCtx: () => SmithersCtx<Schema> }}
   */
  export function createSmithersContext() {
      /** @type {React.Context<SmithersCtx<Schema> | null>} */
      const Context = React.createContext(null);
      Context.displayName = "SmithersContext";
      /**
     * @returns {SmithersCtx<Schema>}
     */
      function useCtx() {
          const ctx = React.useContext(Context);
          if (!ctx) {
              throw new SmithersError("CONTEXT_OUTSIDE_WORKFLOW", "useCtx() must be called inside a <Workflow> created by createSmithers()");
          }
          return ctx;
      }
      return { SmithersContext: Context, useCtx };
  }
  ```

  ```js Task.js theme={"theme":{"light":"github-light","dark":"github-dark"}}
  // @smithers-type-exports-begin
  /**
   * @template D
   * @typedef {import("./InferDeps.ts").InferDeps<D>} InferDeps
   */
  /** @typedef {import("./OutputTarget.ts").OutputTarget} OutputTarget */
  // @smithers-type-exports-end

  import React from "react";
  import { renderToStaticMarkup } from "react-dom/server";
  import { markdownComponents } from "../markdownComponents.js";
  import { zodSchemaToJsonExample } from "../zod-to-example.js";
  import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
  import { SmithersContext } from "@smithers-orchestrator/react-reconciler/context";
  import { AspectContext } from "../aspects/AspectContext.js";
  import { AntigravityAgent } from "@smithers-orchestrator/agents/AntigravityAgent";
  import { ClaudeCodeAgent } from "@smithers-orchestrator/agents/ClaudeCodeAgent";
  import { GeminiAgent } from "@smithers-orchestrator/agents/GeminiAgent";
  import { PiAgent } from "@smithers-orchestrator/agents/PiAgent";
  /** @typedef {import("@smithers-orchestrator/agents/AgentLike").AgentLike} AgentLike */
  /** @typedef {import("./DepsSpec.ts").DepsSpec} DepsSpec */
  /**
   * @template Row, Output, D
   * @typedef {import("./TaskProps.ts").TaskProps<Row, Output, D>} TaskProps
   */

  /**
   * Reverse the HTML-entity escaping that renderToStaticMarkup applies to text
   * (& < > " '). Without this, a prompt like `a < b && c` reaches the agent as
   * `a &lt; b &amp;&amp; c`, and injected JSON schema hints (`"key"`) arrive as
   * `&quot;key&quot;`, both of which corrupt the agent-facing text.
   * `&amp;` is decoded last so an escaped literal (`&amp;lt;`, meaning the source
   * text `&lt;`) is not double-decoded into `<`.
   * @param {string} html
   * @returns {string}
   */
  function decodeHtmlEntities(html) {
      return html
          .replace(/&lt;/g, "<")
          .replace(/&gt;/g, ">")
          .replace(/&quot;/g, '"')
          .replace(/&#x27;/g, "'")
          .replace(/&#39;/g, "'")
          .replace(/&amp;/g, "&");
  }
  /**
   * Render a prompt React node to plain markdown text.
   *
   * If the prompt is a React element (e.g. a compiled MDX component), we inject
   * `markdownComponents` via the standard MDX `components` prop so that
   * renderToStaticMarkup outputs clean markdown instead of HTML. The static
   * render entity-escapes all text, so we decode the entities back to literal
   * characters before handing the prompt to the agent.
   * @param {unknown} prompt
   * @returns {string}
   */
  export function renderPromptToText(prompt) {
      if (prompt == null)
          return "";
      if (typeof prompt === "string")
          return prompt;
      if (typeof prompt === "number")
          return String(prompt);
      try {
          let element;
          if (React.isValidElement(prompt)) {
              // Inject markdown components into the element so MDX components
              // render fragments instead of HTML tags.
              element = React.cloneElement(prompt, {
                  components: markdownComponents,
              });
          }
          else {
              element = React.createElement(React.Fragment, null, prompt);
          }
          return decodeHtmlEntities(renderToStaticMarkup(element))
              .replace(/\n{3,}/g, "\n\n")
              .trim();
      }
      catch (err) {
          const result = String(prompt ?? "");
          if (result === "[object Object]") {
              throw new SmithersError("MDX_PRELOAD_INACTIVE", `MDX prompt could not be rendered — the prompt resolved to [object Object] instead of a React component.\n\n` +
                  `This usually means the MDX preload is not active. Common causes:\n` +
                  `  • bunfig.toml uses [run] preload instead of top-level preload (the [run] section doesn't apply to dynamic imports)\n` +
                  `  • bunfig.toml is not in the current working directory\n` +
                  `  • mdxPlugin() is not registered in the preload script\n` +
                  `  • The MDX file is imported without a default import (use: import MyPrompt from "./prompt.mdx")\n\n` +
                  `Original error: ${err instanceof Error ? err.message : String(err)}`);
          }
          return result;
      }
  }
  /**
   * @param {unknown} value
   * @returns {value is import("zod").ZodObject<import("zod").ZodRawShape>}
   */
  function isZodObject(value) {
      return Boolean(value && typeof value === "object" && "shape" in value);
  }
  /**
   * @param {DepsSpec | undefined} deps
   * @param {Record<string, string> | undefined} needs
   * @returns {string[] | undefined}
   */
  function deriveDepNodeIds(deps, needs) {
      if (!deps)
          return undefined;
      const ids = new Set();
      for (const key of Object.keys(deps)) {
          const nodeId = needs?.[key] ?? key;
          if (nodeId)
              ids.add(nodeId);
      }
      return ids.size > 0 ? [...ids] : undefined;
  }
  /**
   * @param {string[] | undefined} dependsOn
   * @param {string[] | undefined} depNodeIds
   * @returns {string[] | undefined}
   */
  function mergeDependsOn(dependsOn, depNodeIds) {
      const merged = new Set();
      for (const id of dependsOn ?? [])
          merged.add(id);
      for (const id of depNodeIds ?? [])
          merged.add(id);
      return merged.size > 0 ? [...merged] : undefined;
  }
  /**
   * @param {any} ctx
   * @param {DepsSpec | undefined} deps
   * @param {Record<string, string> | undefined} needs
   * @param {boolean | undefined} depsOptional
   * @returns {Record<string, unknown> | null}
   */
  function resolveDeps(ctx, deps, needs, depsOptional) {
      if (!deps)
          return Object.create(null);
      const keys = Object.keys(deps);
      if (keys.length === 0)
          return Object.create(null);
      const resolved = Object.create(null);
      for (const key of keys) {
          const target = deps[key];
          const nodeId = needs?.[key] ?? key;
          const value = ctx.outputMaybe(target, { nodeId });
          if (value === undefined) {
              // Optional deps mode: omit unresolved keys instead of deferring the
              // whole task. Used when the task is already gated by `needs`/`dependsOn`
              // and upstream tasks may legitimately fail (continueOnFail) without
              // producing an output row.
              if (depsOptional)
                  continue;
              return null;
          }
          resolved[key] = value;
      }
      return resolved;
  }
  /**
   * @param {AgentLike} agent
   * @param {string[] | undefined} allowTools
   * @returns {AgentLike}
   */
  function applyCliToolAllowlist(agent, allowTools) {
      if (!allowTools) {
          return agent;
      }
      if (agent instanceof ClaudeCodeAgent) {
          const opts = { ...agent.opts };
          if (allowTools.length === 0) {
              return new ClaudeCodeAgent({
                  ...opts,
                  allowedTools: [],
                  tools: "",
              });
          }
          return new ClaudeCodeAgent({
              ...opts,
              allowedTools: [...allowTools],
          });
      }
      if (agent instanceof PiAgent) {
          const opts = { ...agent.opts };
          if (allowTools.length === 0) {
              return new PiAgent({
                  ...opts,
                  tools: [],
                  noTools: true,
              });
          }
          return new PiAgent({
              ...opts,
              tools: [...allowTools],
              noTools: false,
          });
      }
      if (agent instanceof GeminiAgent) {
          const opts = { ...agent.opts };
          return new GeminiAgent({
              ...opts,
              allowedTools: [...allowTools],
          });
      }
      if (agent instanceof AntigravityAgent) {
          const opts = { ...agent.opts };
          return new AntigravityAgent({
              ...opts,
              allowedTools: [...allowTools],
          });
      }
      return agent;
  }
  /**
   * @param {unknown} ctx
   * @param {string[] | undefined} allowTools
   * @returns {string[] | undefined}
   */
  function resolveCliToolAllowlist(ctx, allowTools) {
      if (allowTools !== undefined) {
          return allowTools;
      }
      const cliAgentToolsDefault = ctx && typeof ctx === "object"
          ? ctx.__smithersRuntime?.cliAgentToolsDefault
          : undefined;
      return cliAgentToolsDefault === "explicit-only" ? [] : undefined;
  }
  /**
   * @template Row, Output, D
   * @param {TaskProps<Row, Output, D>} props
   * @returns {React.ReactElement | null}
   */
  export function Task(props) {
      const { children, agent, fallbackAgent, deps, depsOptional, ...rest } = props;
      const taskContext = props.smithersContext ?? SmithersContext;
      const ctx = React.useContext(taskContext);
      const aspectCtx = React.useContext(AspectContext);
      const depNodeIds = deriveDepNodeIds(deps, rest.needs);
      if (deps && !ctx) {
          throw new SmithersError("CONTEXT_OUTSIDE_WORKFLOW", "Task deps require a workflow context. Build the workflow with createSmithers().");
      }
      const resolvedDeps = deps ? resolveDeps(ctx, deps, rest.needs, depsOptional) : undefined;
      if (deps && resolvedDeps == null) {
          // Deps not yet available — component defers until upstream tasks complete.
          // This is normal reactive behavior; the task will re-render once deps are
          // ready. Record the deferral so the engine can distinguish a transient wait
          // from a permanent one: a deferral that survives to quiescence means a
          // dependency that can never resolve (e.g. a deps key that maps to a node id
          // no task produces), which would otherwise be a silent skip.
          ctx?.recordDeferredDep?.(props.id, depNodeIds ?? []);
          return null;
      }
      // Build aspect metadata to attach to the task element so the engine can
      // enforce budgets and track metrics at execution time.
      const aspectMeta = aspectCtx ? buildAspectMeta(aspectCtx) : undefined;
      const agentChain = Array.isArray(agent)
          ? fallbackAgent
              ? [...agent, fallbackAgent]
              : agent
          : agent && fallbackAgent
              ? [agent, fallbackAgent]
              : agent;
      const effectiveAllowTools = resolveCliToolAllowlist(ctx, rest.allowTools);
      const restrictedAgentChain = Array.isArray(agentChain)
          ? agentChain.map((entry) => applyCliToolAllowlist(entry, effectiveAllowTools))
          : agentChain
              ? applyCliToolAllowlist(agentChain, effectiveAllowTools)
              : agentChain;
      const nextDependsOn = mergeDependsOn(rest.dependsOn, depNodeIds);
      const childValue = typeof children === "function" && (agent || deps)
          ? children(resolvedDeps ?? Object.create(null))
          : children;
      if (agent) {
          // Auto-inject `schema` prop into React element children when output is a ZodObject
          let childElement = childValue;
          const schemaForInjection = props.outputSchema ??
              (isZodObject(props.output) ? props.output : undefined);
          if (React.isValidElement(childValue) && schemaForInjection) {
              childElement = React.cloneElement(childValue, {
                  schema: zodSchemaToJsonExample(schemaForInjection),
              });
          }
          const prompt = renderPromptToText(childElement);
          return React.createElement("smithers:task", {
              ...rest,
              dependsOn: nextDependsOn,
              waitAsync: rest.async === true,
              agent: restrictedAgentChain,
              __smithersKind: "agent",
              ...aspectMeta,
          }, prompt);
      }
      if (typeof children === "function" && !deps) {
          const nextProps = {
              ...rest,
              dependsOn: nextDependsOn,
              waitAsync: rest.async === true,
              __smithersKind: "compute",
              __smithersComputeFn: children,
              ...aspectMeta,
          };
          return React.createElement("smithers:task", nextProps, null);
      }
      const nextProps = {
          ...rest,
          dependsOn: nextDependsOn,
          waitAsync: rest.async === true,
          __smithersKind: "static",
          __smithersPayload: childValue,
          __payload: childValue,
          ...aspectMeta,
      };
      return React.createElement("smithers:task", nextProps, null);
  }
  /**
   * Build the __aspects metadata object from the current AspectContext.
   * This is attached to the smithers:task element props so the engine can read
   * budgets and tracking config at execution time.
   * @param {{
   *     tokenBudget?: unknown;
   *     latencySlo?: unknown;
   *     tracking?: unknown;
   *     accumulator?: unknown;
   * }} aspectCtx
   * @returns {{ __aspects: Record<string, unknown> }}
   */
  function buildAspectMeta(aspectCtx) {
      return {
          __aspects: {
              tokenBudget: aspectCtx.tokenBudget,
              latencySlo: aspectCtx.latencySlo,
              tracking: aspectCtx.tracking,
              accumulator: aspectCtx.accumulator,
          },
      };
  }
  ```

  ```js Ralph.js theme={"theme":{"light":"github-light","dark":"github-dark"}}
  // @smithers-type-exports-begin
  /** @typedef {import("./RalphProps.ts").RalphProps} RalphProps */
  // @smithers-type-exports-end

  import React from "react";
  /** @typedef {import("./LoopProps.ts").LoopProps} LoopProps */

  /**
   * @param {LoopProps} props
   */
  export function Loop(props) {
      if (props.skipIf)
          return null;
      // Sanitize to the loop's host props (align with other structural components);
      // key/skipIf are React/control props and children are passed separately.
      const next = {
          id: props.id,
          until: props.until,
          maxIterations: props.maxIterations,
          onMaxReached: props.onMaxReached,
          continueAsNewEvery: props.continueAsNewEvery,
      };
      return React.createElement("smithers:ralph", next, props.children);
  }
  /** @deprecated Use `Loop` instead. */
  export const Ralph = Loop;
  ```

  ```js Sequence.js theme={"theme":{"light":"github-light","dark":"github-dark"}}
  import React from "react";
  /** @typedef {import("./SequenceProps.ts").SequenceProps} SequenceProps */

  /**
   * @param {SequenceProps} props
   */
  export function Sequence(props) {
      if (props.skipIf)
          return null;
      // Sequence carries only a display label; pass a sanitized bag (align with
      // the sanitizing structural components) so control props don't leak through.
      // `label` names the phase group in run views (graph, the Claude /workflows
      // mirror) and is preserved in the persisted frame XML.
      const next = props.label === undefined ? {} : { label: props.label };
      return React.createElement("smithers:sequence", next, props.children);
  }
  ```

  ```js Timer.js theme={"theme":{"light":"github-light","dark":"github-dark"}}
  import React from "react";
  import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
  /** @typedef {import("./TimerProps.ts").TimerProps} TimerProps */

  /**
   * @param {TimerProps} props
   */
  export function Timer(props) {
      if (props.skipIf)
          return null;
      const hasDuration = typeof props.duration === "string" && props.duration.trim().length > 0;
      const hasUntil = props.until !== undefined && props.until !== null && String(props.until).trim().length > 0;
      if ((hasDuration ? 1 : 0) + (hasUntil ? 1 : 0) !== 1) {
          throw new SmithersError("INVALID_INPUT", `<Timer id="${props.id}"> requires exactly one of "duration" or "until".`);
      }
      if (props.every !== undefined) {
          throw new SmithersError("INVALID_INPUT", `<Timer id="${props.id}"> does not support "every" yet. Recurring timers ship in phase 2.`);
      }
      const untilIso = props.until instanceof Date
          ? props.until.toISOString()
          : typeof props.until === "string"
              ? props.until
              : undefined;
      const timerMeta = {
          timer: true,
          ...(hasDuration ? { duration: props.duration } : {}),
          ...(hasUntil ? { until: untilIso } : {}),
          ...props.meta,
      };
      return React.createElement("smithers:timer", {
          id: props.id,
          key: props.key,
          duration: props.duration,
          until: untilIso,
          dependsOn: props.dependsOn,
          needs: props.needs,
          label: props.label ?? `timer:${props.id}`,
          meta: Object.keys(timerMeta).length > 0 ? timerMeta : undefined,
          __smithersTimerDuration: props.duration,
          __smithersTimerUntil: untilIso,
      });
  }
  ```

  ```ts PollerProps.ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
  import type React from "react";
  import type { AgentLike } from "@smithers-orchestrator/agents/AgentLike";
  import type { OutputTarget } from "./OutputTarget.ts";

  export type PollerProps = {
  	/** ID prefix for generated task/component ids. */
  	id?: string;
  	/** Agent or compute function that checks the condition. */
  	check: AgentLike | (() => unknown | Promise<unknown>);
  	/** Output schema for the check result. Must include `satisfied: boolean`. */
  	checkOutput: OutputTarget;
  	/** Maximum poll attempts. Default 30. */
  	maxAttempts?: number;
  	/** Strategy used to scale the inter-attempt delay. Default "fixed". */
  	backoff?: "fixed" | "linear" | "exponential";
  	/**
  	 * Base delay in milliseconds BETWEEN poll attempts; the first attempt runs
  	 * immediately. Enforced by a durable `<Timer>`. Default 5000.
  	 */
  	intervalMs?: number;
  	/** Timeout in ms for a single check attempt. Unbounded when unset. */
  	checkTimeoutMs?: number;
  	/** Behavior when maxAttempts is reached. Default "fail". */
  	onTimeout?: "fail" | "return-last";
  	/** Skip the entire component. */
  	skipIf?: boolean;
  	/** Prompt/condition description for the check agent. */
  	children?: React.ReactNode;
  };
  ```
</CodeGroup>
