# log_action

> Record something you (or the user) just did or observed on a growth item: what happened (one sentence is enough), the new stage only if you know it…

URL: https://mcp-docs.opennomos.com/docs/tools/growth/log_action

> Record something you (or the user) just did or observed on a growth item: what happened (one sentence is enough), the new stage only if you know it changed, when to follow up, and any outcome. Pass an opportunity id or a URL (an unknown URL is tracked automatically); with neither, only the action is recorded. The server never infers a stage from the action. Do not put private message text or personal data in summaries. Use idempotency_key to make retries safe.

| Field | Value |
| --- | --- |
| Pack | Core (`core`) |
| Estimated cost per call | $0 |
| Monthly pack limit | none |
| Project | resolved: explicit `project` → header → credential default → workspace default |

## Parameters

| Parameter | Required | Type | Description |
| --- | --- | --- | --- |
| `project` | no | `string` | Project slug or id. Omit to use the credential or workspace default. |
| `opportunity_id` | no | `string` | The opportunity this is about (id from track or get_briefing). Use this or url, not both. |
| `url` | no | `string (≤ 2000 chars)` | Or the post/profile URL it is about; an unknown URL is tracked automatically. |
| `title` | no | `string (≤ 200 chars)` | Title for a newly tracked URL. |
| `action` | yes | `object` | What was done or observed. |
| `action.kind` | no | `string (≤ 50 chars)` | What kind of action, e.g. 'comment', 'dm', 'post', 'email'. Default 'action'. |
| `action.summary` | yes | `string` | One sentence on what happened, e.g. 'Replied with a comparison table'. No private message text. |
| `action.channel` | no | `string (≤ 100 chars)` | Where, e.g. 'reddit', 'x', 'xiaohongshu'. |
| `action.url` | no | `string (≤ 2000 chars)` | Link to what you posted, e.g. your comment URL. |
| `action.occurred_at` | no | `ISO datetime` | When it happened (ISO time). Default: now. |
| `stage` | no | `"discovered"` \| `"contacted"` \| `"replied"` \| `"willing_to_talk"` \| `"converted"` \| `"dropped"` | New stage, only if you know it changed: discovered, contacted, replied, willing_to_talk, converted or dropped. |
| `next_followup` | no | `ISO datetime \| {in_days} \| {at} \| null` | When to follow up: \{"in_days": 3\}, \{"at": ISO time\}, or an ISO time. null clears it. Default: a stage-based suggestion. |
| `experiment_id` | no | `string` | Attach this action to an experiment (id from track). |
| `link_code` | no | `string (≤ 32 chars)` | Code of a short link used in this action (from track type=link), for click attribution. |
| `outcome` | no | `object` | A result of this action, if there is one. |
| `outcome.type` | yes | `string (≤ 60 chars)` | What was achieved, e.g. 'signup', 'reply', 'call_booked'. |
| `outcome.value` | no | `number` | Amount, e.g. 1 or 49.9. |
| `outcome.unit` | no | `string (≤ 20 chars)` | Unit of value, e.g. 'usd', 'count'. |
| `outcome.evidence` | no | `string` | How you know, e.g. 'Reply in thread from the author'. |
| `note` | no | `string` | Extra context, one or two sentences. |
| `idempotency_key` | no | `string (≤ 100 chars)` | Any unique string; repeating the call with the same key does not record it twice. |

## Example call

```json
{
  "project": "acme-crm",
  "url": "https://www.reddit.com/r/SaaS/comments/1abc23/looking_for_a_lightweight_crm",
  "action": {
    "kind": "comment",
    "summary": "Replied with a comparison table",
    "channel": "reddit"
  },
  "stage": "contacted",
  "experiment_id": "exp_7Hn2LpWc0",
  "idempotency_key": "2026-10-16-comment-1abc23"
}
```

REST: `POST https://service.opennomos.com/v1/tools/log_action` with that JSON as the body. MCP: `tools/call` with `name: "log_action"` and that JSON as `arguments`.

## Example result

Values are illustrative; the shape comes from the service code.

```json
{
  "project": {
    "id": "prj_k3Vd9aQx2",
    "slug": "acme-crm"
  },
  "action_id": "act_Lw03pKd8",
  "opportunity": {
    "id": "opp_Qm4x9TnB1",
    "stage": "contacted",
    "next_followup_at": "2026-10-19T09:30:00.000Z",
    "events": 3
  },
  "experiment_progress": {
    "id": "exp_7Hn2LpWc0",
    "metric": "willing_to_talk",
    "current": 1,
    "target": 3,
    "deadline_at": "2026-10-30T09:00:00.000Z"
  },
  "hints": []
}
```

## Notes

- The *write-back* half of the loop. OpenNomos never posts, replies or messages anyone: you (or the user) do that elsewhere, then record it here.
- Pass `opportunity_id` or `url` (an unknown URL is tracked automatically). With neither, only the action is recorded.
- **Defaults**: the server never infers a stage. If `stage` is omitted the stage stays. If `next_followup` is omitted the follow-up time stays **unless the stage actually changed**, in which case it is set from the new stage: `contacted` +3 days, `replied` +2, `willing_to_talk` +2, `converted` and `dropped` clear it. `next_followup: null` clears it. Details in [log_action defaults](https://mcp-docs.opennomos.com/docs/growth-loop/log-action.md).
- `outcome` records a result (`type`, optional `value`, `unit`, `evidence`). It is counted as attributed (`direct`) only when it is tied to an opportunity or a short link; otherwise it is `unknown`. See [Attribution](https://mcp-docs.opennomos.com/docs/growth-loop/attribution.md).
- `idempotency_key` makes retries safe: repeating a call with the same key replays the first result (with `duplicate: true`) instead of recording twice. `experiment_progress` appears when the action is attached to an experiment.
- `hints` can contain a one-line warning, for example when `outcome.type` is `willing_to_talk` but the stage was not changed.
- Do not put private message text or personal data in `summary` or `note`.

## Errors

Rejected calls (bad input, unknown ids, rate limits) return `message` and `code`; over MCP the result has `isError: true`. See [Errors](https://mcp-docs.opennomos.com/docs/reference/errors.md).
