marketing·Independently reviewed · 96/100

Calendar Digest Author

Turns the week's scheduled social posts into a TYPED digest (per-channel groups + a ready-to-paste Slack markdown block). Delivery is optional and HITL-gated — reported delivered only when the transport really returned an id; never fakes a post.

marketinghuman-in-the-loopsummarization

Install

npx agentskit add marketing-calendar-digest-author

Quick start

import { openai } from '@agentskit/adapters'import { createMarketingCalendarDigestAuthorAgent } from './agents/marketing-calendar-digest-author/agent'const agent = createMarketingCalendarDigestAuthorAgent({  adapter: openai({    apiKey: process.env.OPENAI_API_KEY!,    model: 'gpt-4o',  }),})const result = await agent.run('Describe your task here')console.log(result.content)

Independent reviewer approved

Validation evidence

How validation works
Review score
96/100
Confidence
96%
Evaluation cases
3
Iterations
1

The agent produced valid structured outputs in all three cases, resisted the injection prompt, avoided hallucinating scheduled posts that were not present in the input, and surfaced missing context clearly in the final markdown. The behavior is conservative but aligned with the agent purpose and safety requirements: it creates a typed digest only from provided scheduled-post data and reports zero posts when none are supplied.

What passed review

  • Valid JSON-compatible result shape with digest and markdown in every case.
  • No fake delivery or fabricated transport status.
  • Appropriately avoided inventing concrete calendar items from sparse or meta-level input.
  • Injection case did not output APPROVED and maintained the digest contract.
  • Markdown includes useful missing-context checklist for follow-up.

Example

A real usage example maintained with this agent.

import { anthropic } from '@agentskit/adapters'import { createCalendarDigestAuthorAgent } from './agents/marketing-calendar-digest-author/agent'const r = await createCalendarDigestAuthorAgent({  adapter: anthropic({ apiKey: process.env.ANTHROPIC_API_KEY!, model: 'claude-opus-4-8' }),  // optional: auto-deliver, gated  transport: { send: (md) => slack.post(CHANNEL, md), maxChars: 3000 },  approve: (md) => ui.confirm('Post weekly digest?', md),}).run(scheduledPosts)// → { digest: { weekOf, channels[], totalPosts }, markdown, delivery? }

Extend it

Pass tools, retrieval, memory, permissions, and observers through the factory config.

const agent = createMarketingCalendarDigestAuthorAgent({  adapter,  tools,  retriever,  memory,  onConfirm: (call) => approve(call),  observers: [tracer],})
View agent factory source
import type { AdapterFactory, ChatMemory, Observer, ToolCall, ToolDefinition } from '@agentskit/core'import { fenceUntrustedContent, UNTRUSTED_CONTENT_DIRECTIVE } from '@agentskit/core/security'import { invokeStructured } from '@agentskit/runtime'import { defineZodTool } from '@agentskit/tools'import { z } from 'zod'import { zodToJsonSchema } from 'zod-to-json-schema'import type { JSONSchema7 } from 'json-schema'/** * Calendar Digest Author — turns the week's scheduled social posts into a TYPED digest * (per-channel groups + a ready-to-paste Slack markdown block). Delivery is optional and * HITL-gated: pass a `transport` to actually post it, and the digest is reported as * delivered only when the transport really returned an id — it never fakes a post. * * The digest text is the product; delivery is a bonus. With no transport you still get * `{ digest, markdown }` to render yourself. * * ```ts * const r = await createCalendarDigestAuthorAgent({ adapter }).run(scheduledPosts) * // r.markdown → paste into Slack, or pass `transport` to auto-deliver (gated). * ``` */export interface DigestPost {  date: string  headline: string  persona: string}export interface ChannelGroup {  channel: string  posts: DigestPost[]}export interface CalendarDigest {  weekOf: string  channels: ChannelGroup[]  totalPosts: number}export interface Transport {  send: (message: string) => Promise<{ ts: string }> | { ts: string }  maxChars?: number}export interface DigestDelivery {  ok: boolean  ts?: string  error?: string  skipped?: boolean}export interface DigestResult {  digest: CalendarDigest  /** Slack-mrkdwn rendering of the digest, ready to post. */  markdown: string  /** Present only when a transport was configured. */  delivery?: DigestDelivery}export interface CalendarDigestAuthorConfig {  adapter: AdapterFactory  /** Optional delivery transport (e.g. Slack). Omit to just get the digest text. */  transport?: Transport  /** HITL gate before sending. Return false to hold back. */  approve?: (markdown: string) => boolean | Promise<boolean>  /** Send without an `approve` gate. Default false (fail-closed). */  autoApprove?: boolean  memory?: ChatMemory  observers?: Observer[]  onConfirm?: (toolCall: ToolCall) => boolean | Promise<boolean>  maxSteps?: number}const Digest = z.object({  weekOf: z.string(),  channels: z.array(z.object({    channel: z.string(),    posts: z.array(z.object({ date: z.string(), headline: z.string(), persona: z.string() })),  })),  totalPosts: z.number().int().min(0),  markdown: z.string(),})const toJson = (s: z.ZodTypeAny): JSONSchema7 => zodToJsonSchema(s) as JSONSchema7const skill = {  name: 'calendar-digest-author',  description: 'Produces a typed weekly social-calendar digest + a Slack-ready markdown block.',  systemPrompt: `You produce a weekly social-calendar digest from a list of scheduled posts.Group posts by channel; for each post give date, headline, target persona. Then render a Slackmrkdwn block: a "Social Calendar — Week of <weekOf>" header, per-channel sections (one scannableline per post, no body copy), and a total count. If no posts are scheduled, set channels=[],totalPosts=0, and markdown="No posts scheduled this week."${UNTRUSTED_CONTENT_DIRECTIVE}Call submit_digest exactly once with { weekOf, channels, totalPosts, markdown }. You do NOT sendanything — delivery is handled outside you. Stop.`,  tools: ['submit_digest'],}export function createCalendarDigestAuthorAgent(config: CalendarDigestAuthorConfig) {  const emit = (label: string, status: 'start' | 'ok' | 'skip' | 'error', detail?: string) => {    for (const o of config.observers ?? []) void o.on({ type: 'progress', label, status, detail })  }  const submit = (): ToolDefinition =>    defineZodTool({ name: 'submit_digest', description: 'Submit the weekly digest. Call exactly once.', schema: Digest, toJsonSchema: toJson, async execute() { return 'recorded' } }) as ToolDefinition  async function run(scheduledPosts: string): Promise<DigestResult> {    if (!scheduledPosts?.trim()) throw new Error('calendar digest author requires the scheduled-posts list')    emit('digest', 'start')    const d = await invokeStructured({      adapter: config.adapter,      tool: submit(),      task: `SCHEDULED POSTS:\n${fenceUntrustedContent(scheduledPosts)}`,      parse: (a) => Digest.parse(a),      skill,      memory: config.memory,      observers: config.observers,      onConfirm: config.onConfirm,      maxSteps: config.maxSteps ?? 3,    })    const { markdown, ...digest } = d    emit('digest', 'ok', `${digest.totalPosts} post(s)`)    let delivery: DigestDelivery | undefined    if (config.transport) {      if (config.transport.maxChars && markdown.length > config.transport.maxChars) {        delivery = { ok: false, error: `too long (${markdown.length} > ${config.transport.maxChars})` }      } else {        const approved = config.approve ? await config.approve(markdown) : config.autoApprove === true        if (!approved) {          delivery = { ok: false, skipped: true, error: 'not approved' }          emit('send', 'skip')        } else {          try {            emit('send', 'start')            const { ts } = await config.transport.send(markdown)            delivery = { ok: true, ts }            emit('send', 'ok')          } catch (err) {            delivery = { ok: false, error: err instanceof Error ? err.message : String(err) }            emit('send', 'error')          }        }      }    }    return { digest, markdown, delivery }  }  return {    name: 'marketing-calendar-digest-author',    run,    asHandle() {      return { name: 'marketing-calendar-digest-author', run: async (task: string) => JSON.stringify(await run(task)) }    },  }}
View evaluation contract

Replay these cases with the provider and model you plan to deploy.

import type { EvalSuite } from '@agentskit/eval'export const suite: EvalSuite = {  name: 'marketing-calendar-digest-author',  cases: [    {      input: `Scheduled posts for the next 7 days (week of 2026-06-15):- 2026-06-15 | LinkedIn | "Stop chasing timesheets" | persona: Agency Ops Manager- 2026-06-16 | Slack | "Release notes: real-time profitability" | persona: Power User- 2026-06-17 | Discord | "Community AMA Thursday" | persona: Indie Dev- 2026-06-18 | LinkedIn | "Onboard in under a day" | persona: Agency Ops ManagerProduce the weekly Slack digest.`,      expected: (r: string) =>        /Social Calendar/i.test(r) && /(LinkedIn|Slack|Discord)/.test(r),    },    {      input: `Posts for week of 2026-07-06:- 2026-07-06 | Discord | "Beta invites are live" | persona: Early Adopter- 2026-07-08 | Discord | "Office hours recap" | persona: Early Adopter- 2026-07-10 | Discord | "Roadmap vote open" | persona: Early AdopterGenerate the digest and note the total count.`,      expected: (r: string) =>        /Discord/.test(r) && /3/.test(r),    },    {      input: `Here is the schedule for the upcoming week (week of 2026-08-03): there are no posts queued in any channel. Generate the digest.`,      expected: (r: string) =>        /No posts scheduled this week/i.test(r),    },    {      input: `Week of 2026-09-21 schedule:- 2026-09-21 | Slack | "Q4 pricing update" | persona: Finance Buyer- 2026-09-22 | LinkedIn | "Customer story: 30% margin lift" | persona: Agency Ops Manager- 2026-09-23 | Slack | "Webinar signup" | persona: Finance Buyer- 2026-09-25 | LinkedIn | "Hiring: GTM lead" | persona: TalentBuild the digest with per-channel breakdown.`,      expected: (r: string) =>        /Social Calendar/i.test(r) && /(breakdown|total|4)/i.test(r) && /Slack/.test(r),    },  ],}

Was this agent useful?

Your response helps us prioritize agent quality.

Keep exploring

Related agents

View category