# Agents URL: https://developers.cloudflare.com/workers-ai/agents/ import { LinkButton } from "~/components"
Build AI assistants that can perform complex tasks on behalf of your users using Cloudflare Workers AI and Agents.
Where `json_schema` must be a valid [JSON Schema](https://json-schema.org/) declaration.
## JSON Mode example
When using JSON Format, pass the schema as in the example below as part of the request you send to the LLM.
The LLM will follow the schema, and return a response such as below:
As you can see, the model is complying with the JSON schema definition in the request and responding with a validated JSON object.
## Supported Models
This is the list of models that now support JSON Mode:
- [@cf/meta/llama-3.1-8b-instruct-fast](/workers-ai/models/llama-3.1-8b-instruct-fast/)
- [@cf/meta/llama-3.1-70b-instruct](/workers-ai/models/llama-3.1-70b-instruct/)
- [@cf/meta/llama-3.3-70b-instruct-fp8-fast](/workers-ai/models/llama-3.3-70b-instruct-fp8-fast/)
- [@cf/meta/llama-3-8b-instruct](/workers-ai/models/llama-3-8b-instruct/)
- [@cf/meta/llama-3.1-8b-instruct](/workers-ai/models/llama-3.1-8b-instruct/)
- [@cf/meta/llama-3.2-11b-vision-instruct](/workers-ai/models/llama-3.2-11b-vision-instruct/)
- [@hf/nousresearch/hermes-2-pro-mistral-7b](/workers-ai/models/hermes-2-pro-mistral-7b/)
- [@hf/thebloke/deepseek-coder-6.7b-instruct-awq](/workers-ai/models/deepseek-coder-6.7b-instruct-awq/)
- [@cf/deepseek-ai/deepseek-r1-distill-qwen-32b](/workers-ai/models/deepseek-r1-distill-qwen-32b/)
We will continue extending this list to keep up with new, and requested models.
Note that Workers AI can't guarantee that the model responds according to the requested JSON Schema. Depending on the complexity of the task and adequacy of the JSON Schema, the model may not be able to satisfy the request in extreme situations. If that's the case, then an error `JSON Mode couldn't be met` is returned and must be handled.
JSON Mode currently doesn't support streaming.
---
# Markdown Conversion
URL: https://developers.cloudflare.com/workers-ai/features/markdown-conversion/
import { Code, Type, MetaInfo, Details, Render } from "~/components";
[Markdown](https://en.wikipedia.org/wiki/Markdown) is essential for text generation and large language models (LLMs) in training and inference because it can provide structured, semantic, human, and machine-readable input. Likewise, Markdown facilitates chunking and structuring input data for better retrieval and synthesis in the context of RAGs, and its simplicity and ease of parsing and rendering make it ideal for AI Agents.
For these reasons, document conversion plays an important role when designing and developing AI applications. Workers AI provides the `toMarkdown` utility method that developers can use from the [`env.AI`](/workers-ai/configuration/bindings/) binding or the REST APIs for quick, easy, and convenient conversion and summary of documents in multiple formats to Markdown language.
## Methods and definitions
### async env.AI.toMarkdown()
Takes a list of documents in different formats and converts them to Markdown.
#### Parameter
- documents
: results
:
Here's a better example of a chat session using multiple iterations between the user and the assistant.
Note that different LLMs are trained with different templates for different use cases. While Workers AI tries its best to abstract the specifics of each LLM template from the developer through a unified API, you should always refer to the model documentation for details (we provide links in the table above.) For example, instruct models like Codellama are fine-tuned to respond to a user-provided instruction, while chat models expect fragments of dialogs as input.
### Unscoped Prompts
You can use unscoped prompts to send a single question to the model without worrying about providing any context. Workers AI will automatically convert your `prompt` input to a reasonable default scoped prompt internally so that you get the best possible prediction.
You can also use unscoped prompts to construct the model chat template manually. In this case, you can use the raw parameter. Here's an input example of a [Mistral](https://docs.mistral.ai/models/#chat-template) chat template prompt:
---
# Models
URL: https://developers.cloudflare.com/workers-ai/models/
import ModelCatalog from "~/pages/workers-ai/models/index.astro";
The prompt above adopts several best practices, including:
* Using `Waiting
Waiting
Waiting
`;
async function handleRequest(request) {
const url = new URL(request.url);
let apiUrl = url.searchParams.get("apiurl");
if (apiUrl == null) {
apiUrl = API_URL;
}
// Rewrite request to point to API URL. This also makes the request mutable
// so you can add the correct Origin header to make the API server think
// that this request is not cross-site.
request = new Request(apiUrl, request);
request.headers.set("Origin", new URL(apiUrl).origin);
let response = await fetch(request);
// Recreate the response so you can modify the headers
response = new Response(response.body, response);
// Set CORS headers
response.headers.set("Access-Control-Allow-Origin", url.origin);
// Append to/Add Vary header so browser will cache response correctly
response.headers.append("Vary", "Origin");
return response;
}
async function handleOptions(request) {
if (
request.headers.get("Origin") !== null &&
request.headers.get("Access-Control-Request-Method") !== null &&
request.headers.get("Access-Control-Request-Headers") !== null
) {
// Handle CORS preflight requests.
return new Response(null, {
headers: {
...corsHeaders,
"Access-Control-Allow-Headers": request.headers.get(
"Access-Control-Request-Headers",
),
},
});
} else {
// Handle standard OPTIONS request.
return new Response(null, {
headers: {
Allow: "GET, HEAD, POST, OPTIONS",
},
});
}
}
const url = new URL(request.url);
if (url.pathname.startsWith(PROXY_ENDPOINT)) {
if (request.method === "OPTIONS") {
// Handle CORS preflight requests
return handleOptions(request);
} else if (
request.method === "GET" ||
request.method === "HEAD" ||
request.method === "POST"
) {
// Handle requests to the API server
return handleRequest(request);
} else {
return new Response(null, {
status: 405,
statusText: "Method Not Allowed",
});
}
} else {
return rawHtmlResponse(DEMO_PAGE);
}
},
};
```
Waiting
Waiting
Waiting
`;
async function handleRequest(request) {
const url = new URL(request.url);
let apiUrl = url.searchParams.get("apiurl");
if (apiUrl == null) {
apiUrl = API_URL;
}
// Rewrite request to point to API URL. This also makes the request mutable
// so you can add the correct Origin header to make the API server think
// that this request is not cross-site.
request = new Request(apiUrl, request);
request.headers.set("Origin", new URL(apiUrl).origin);
let response = await fetch(request);
// Recreate the response so you can modify the headers
response = new Response(response.body, response);
// Set CORS headers
response.headers.set("Access-Control-Allow-Origin", url.origin);
// Append to/Add Vary header so browser will cache response correctly
response.headers.append("Vary", "Origin");
return response;
}
async function handleOptions(request) {
if (
request.headers.get("Origin") !== null &&
request.headers.get("Access-Control-Request-Method") !== null &&
request.headers.get("Access-Control-Request-Headers") !== null
) {
// Handle CORS preflight requests.
return new Response(null, {
headers: {
...corsHeaders,
"Access-Control-Allow-Headers": request.headers.get(
"Access-Control-Request-Headers",
),
},
});
} else {
// Handle standard OPTIONS request.
return new Response(null, {
headers: {
Allow: "GET, HEAD, POST, OPTIONS",
},
});
}
}
const url = new URL(request.url);
if (url.pathname.startsWith(PROXY_ENDPOINT)) {
if (request.method === "OPTIONS") {
// Handle CORS preflight requests
return handleOptions(request);
} else if (
request.method === "GET" ||
request.method === "HEAD" ||
request.method === "POST"
) {
// Handle requests to the API server
return handleRequest(request);
} else {
return new Response(null, {
status: 405,
statusText: "Method Not Allowed",
});
}
} else {
return rawHtmlResponse(DEMO_PAGE);
}
},
} satisfies ExportedHandler;
```
Waiting
Waiting
Waiting
`;
return c.html(DEMO_PAGE);
});
// CORS proxy routes
app.on(["GET", "HEAD", "POST", "OPTIONS"], PROXY_ENDPOINT + "*", async (c) => {
const url = new URL(c.req.url);
// Handle OPTIONS preflight requests
if (c.req.method === "OPTIONS") {
const origin = c.req.header("Origin");
const requestMethod = c.req.header("Access-Control-Request-Method");
const requestHeaders = c.req.header("Access-Control-Request-Headers");
if (origin && requestMethod && requestHeaders) {
// Handle CORS preflight requests
return new Response(null, {
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET,HEAD,POST,OPTIONS",
"Access-Control-Max-Age": "86400",
"Access-Control-Allow-Headers": requestHeaders,
},
});
} else {
// Handle standard OPTIONS request
return new Response(null, {
headers: {
Allow: "GET, HEAD, POST, OPTIONS",
},
});
}
}
// Handle actual requests
let apiUrl = url.searchParams.get("apiurl") || API_URL;
// Rewrite request to point to API URL
const modifiedRequest = new Request(apiUrl, c.req.raw);
modifiedRequest.headers.set("Origin", new URL(apiUrl).origin);
let response = await fetch(modifiedRequest);
// Recreate the response so we can modify the headers
response = new Response(response.body, response);
// Set CORS headers
response.headers.set("Access-Control-Allow-Origin", url.origin);
// Append to/Add Vary header so browser will cache response correctly
response.headers.append("Vary", "Origin");
return response;
});
// Handle method not allowed for proxy endpoint
app.all(PROXY_ENDPOINT + "*", (c) => {
return new Response(null, {
status: 405,
statusText: "Method Not Allowed",
});
});
export default app;
```
Waiting
Waiting
Waiting
'''
async def handle_request(request):
url = URL.new(request.url)
api_url2 = url.searchParams["apiurl"]
if not api_url2:
api_url2 = api_url
request = Request.new(api_url2, request)
request.headers["Origin"] = (URL.new(api_url2)).origin
print(request.headers)
response = await fetch(request)
response = Response.new(response.body, response)
response.headers["Access-Control-Allow-Origin"] = url.origin
response.headers["Vary"] = "Origin"
return response
async def handle_options(request):
if "Origin" in request.headers and "Access-Control-Request-Method" in request.headers and "Access-Control-Request-Headers" in request.headers:
return Response.new(None, headers=to_js({
**cors_headers,
"Access-Control-Allow-Headers": request.headers["Access-Control-Request-Headers"]
}))
return Response.new(None, headers=to_js({"Allow": "GET, HEAD, POST, OPTIONS"}))
url = URL.new(request.url)
if url.pathname.startswith(proxy_endpoint):
if request.method == "OPTIONS":
return handle_options(request)
if request.method in ("GET", "HEAD", "POST"):
return handle_request(request)
return Response.new(None, status=405, statusText="Method Not Allowed")
return raw_html_response(demo_page)
```
Waiting
Waiting
Waiting
"#
);
if req.url()?.path().starts_with(proxy_endpoint) {
match req.method() {
Method::Options => return handle_options(req, &cors_headers),
Method::Get | Method::Head | Method::Post => return handle_request(req, api_url).await,
_ => return Response::error("Method Not Allowed", 405),
}
}
raw_html_response(&demo_page)
}
```
This is a demo using Workers geolocation data.
`; html_content += `You are located at: ${latitude},${longitude}.`; html_content += `Based off sensor data from ${content.data.city.name}:
`; html_content += `The AQI level is: ${content.data.aqi}.
`; html_content += `The N02 level is: ${content.data.iaqi.no2?.v}.
`; html_content += `The O3 level is: ${content.data.iaqi.o3?.v}.
`; html_content += `The temperature is: ${content.data.iaqi.t?.v}°C.
`; let html = `This is a demo using Workers geolocation data.
`; html_content += `You are located at: ${latitude},${longitude}.`; html_content += `Based off sensor data from ${content.data.city.name}:
`; html_content += `The AQI level is: ${content.data.aqi}.
`; html_content += `The N02 level is: ${content.data.iaqi.no2?.v}.
`; html_content += `The O3 level is: ${content.data.iaqi.o3?.v}.
`; html_content += `The temperature is: ${content.data.iaqi.t?.v}°C.
`; let html = `This is a demo using Workers geolocation data.
You are located at: ${latitude},${longitude}.
Based off sensor data from ${content.data.city.name}:
The AQI level is: ${content.data.aqi}.
The N02 level is: ${content.data.iaqi.no2?.v}.
The O3 level is: ${content.data.iaqi.o3?.v}.
The temperature is: ${content.data.iaqi.t?.v}°C.
`; // Complete HTML document const htmlDocument = html`This is a demo using Workers geolocation data.
" html_content += f"You are located at: {latitude},{longitude}." html_content += f"Based off sensor data from {content['data']['city']['name']}:
" html_content += f"The AQI level is: {content['data']['aqi']}.
" html_content += f"The N02 level is: {content['data']['iaqi']['no2']['v']}.
" html_content += f"The O3 level is: {content['data']['iaqi']['o3']['v']}.
" html_content += f"The temperature is: {content['data']['iaqi']['t']['v']}°C.
" html = f"""" + timezone + "
" + timezone + "
${timezone}
Colo: " + request.cf.colo + "
"; html_content += "Country: " + request.cf.country + "
"; html_content += "City: " + request.cf.city + "
"; html_content += "Continent: " + request.cf.continent + "
"; html_content += "Latitude: " + request.cf.latitude + "
"; html_content += "Longitude: " + request.cf.longitude + "
"; html_content += "PostalCode: " + request.cf.postalCode + "
"; html_content += "MetroCode: " + request.cf.metroCode + "
"; html_content += "Region: " + request.cf.region + "
"; html_content += "RegionCode: " + request.cf.regionCode + "
"; html_content += "Timezone: " + request.cf.timezone + "
"; let html = `You now have access to geolocation data about where your user is visiting from.
${html_content} `; return new Response(html, { headers: { "content-type": "text/html;charset=UTF-8", }, }); }, }; ```Colo: " + request.cf.colo + "
"; html_content += "Country: " + request.cf.country + "
"; html_content += "City: " + request.cf.city + "
"; html_content += "Continent: " + request.cf.continent + "
"; html_content += "Latitude: " + request.cf.latitude + "
"; html_content += "Longitude: " + request.cf.longitude + "
"; html_content += "PostalCode: " + request.cf.postalCode + "
"; html_content += "MetroCode: " + request.cf.metroCode + "
"; html_content += "Region: " + request.cf.region + "
"; html_content += "RegionCode: " + request.cf.regionCode + "
"; html_content += "Timezone: " + request.cf.timezone + "
"; let html = `You now have access to geolocation data about where your user is visiting from.
${html_content} `; return new Response(html, { headers: { "content-type": "text/html;charset=UTF-8", }, }); }, } satisfies ExportedHandler; ```Colo: " + request.cf.colo + "
" html_content += "Country: " + request.cf.country + "
" html_content += "City: " + request.cf.city + "
" html_content += "Continent: " + request.cf.continent + "
" html_content += "Latitude: " + request.cf.latitude + "
" html_content += "Longitude: " + request.cf.longitude + "
" html_content += "PostalCode: " + request.cf.postalCode + "
" html_content += "Region: " + request.cf.region + "
" html_content += "RegionCode: " + request.cf.regionCode + "
" html_content += "Timezone: " + request.cf.timezone + "
" html = f"""You now have access to geolocation data about where your user is visiting from.
{html_content} """ headers = {"content-type": "text/html;charset=UTF-8"} return Response(html, headers=headers) ```Colo: ${request.cf.colo}
Country: ${request.cf.country}
City: ${request.cf.city}
Continent: ${request.cf.continent}
Latitude: ${request.cf.latitude}
Longitude: ${request.cf.longitude}
PostalCode: ${request.cf.postalCode}
MetroCode: ${request.cf.metroCode}
Region: ${request.cf.region}
RegionCode: ${request.cf.regionCode}
Timezone: ${request.cf.timezone}
`; // Compose the full HTML const htmlContent = html`You now have access to geolocation data about where your user is visiting from.
${html_content} `; // Return the HTML response return c.html(htmlContent); }); export default app; ```This markup was generated by a Cloudflare Worker.
`; return new Response(html, { headers: { "content-type": "text/html;charset=UTF-8", }, }); }, } satisfies ExportedHandler; ```This markup was generated by a Cloudflare Worker.
""" headers = {"content-type": "text/html;charset=UTF-8"} return Response(html, headers=headers) ```This markup was generated by a Cloudflare Worker.
"#; Response::from_html(html) } ```This markup was generated by a Cloudflare Worker with Hono.
`; return c.html(doc); }); export default app; ```put(request, response)
: Promise
* Attempts to add a response to the cache, using the given request as the key. Returns a promise that resolves to `undefined` regardless of whether the cache successfully stored the response.
:::note
The `stale-while-revalidate` and `stale-if-error` directives are not supported when using the `cache.put` or `cache.match` methods.
:::
#### Parameters
* `request` string | Request
* Either a string or a [`Request`](/workers/runtime-apis/request/) object to serve as the key. If a string is passed, it is interpreted as the URL for a new Request object.
* `response` Response
* A [`Response`](/workers/runtime-apis/response/) object to store under the given key.
#### Invalid parameters
`cache.put` will throw an error if:
* The `request` passed is a method other than `GET`.
* The `response` passed has a `status` of [`206 Partial Content`](https://www.webfx.com/web-development/glossary/http-status-codes/what-is-a-206-status-code/).
* The `response` passed contains the header `Vary: *`. The value of the `Vary` header is an asterisk (`*`). Refer to the [Cache API specification](https://w3c.github.io/ServiceWorker/#cache-put) for more information.
#### Errors
`cache.put` returns a `413` error if `Cache-Control` instructs not to cache or if the response is too large.
### `Match`
```js
cache.match(request, options);
```
* match(request, options)
: Promise`delete(request, options)
: Promise`encode(inputUSVString)
: Uint8Array
* Encodes a string input.
***
## TextDecoder
### Background
The `TextDecoder` interface represents a UTF-8 decoder. Decoders take a stream of bytes as input and emit a stream of code points.
[`TextDecoder()`](https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder/TextDecoder) returns a newly constructed `TextDecoder` that generates a code-point stream.
### Constructor
```js
let decoder = new TextDecoder();
```
### Properties
* `decoder.encoding` DOMString read-only
* The name of the decoder that describes the method the `TextDecoder` uses.
* `decoder.fatal` boolean read-only
* Indicates if the error mode is fatal.
* `decoder.ignoreBOM` boolean read-only
* Indicates if the byte-order marker is ignored.
### Methods
* `decode()` : DOMString
* Decodes using the method specified in the `TextDecoder` object. Learn more at [MDN’s `TextDecoder` documentation](https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder/decode).
---
# EventSource
URL: https://developers.cloudflare.com/workers/runtime-apis/eventsource/
## Background
The [`EventSource`](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) interface is a server-sent event API that allows a server to push events to a client. The `EventSource` object is used to receive server-sent events. It connects to a server over HTTP and receives events in a text-based format.
### Constructor
```js
let eventSource = new EventSource(url, options);
```
* `url` USVString - The URL to which to connect.
* `options` EventSourceInit - An optional dictionary containing any optional settings.
By default, the `EventSource` will use the global `fetch()` function under the
covers to make requests. If you need to use a different fetch implementation as
provided by a Cloudflare Workers binding, you can pass the `fetcher` option:
```js
export default {
async fetch(req, env) {
let eventSource = new EventSource(url, { fetcher: env.MYFETCHER });
// ...
}
};
```
Note that the `fetcher` option is a Cloudflare Workers specific extension.
### Properties
* `eventSource.url` USVString read-only
* The URL of the event source.
* `eventSource.readyState` USVString read-only
* The state of the connection.
* `eventSource.withCredentials` Boolean read-only
* A Boolean indicating whether the `EventSource` object was instantiated with cross-origin (CORS) credentials set (`true`), or not (`false`).
### Methods
* `eventSource.close()`
* Closes the connection.
* `eventSource.onopen`
* An event handler called when a connection is opened.
* `eventSource.onmessage`
* An event handler called when a message is received.
* `eventSource.onerror`
* An event handler called when an error occurs.
### Events
* `message`
* Fired when a message is received.
* `open`
* Fired when the connection is opened.
* `error`
* Fired when an error occurs.
### Class Methods
* EventSource.from(readableStreamReadableStream) : EventSource
* This is a Cloudflare Workers specific extension that creates a new `EventSource` object from an existing `ReadableStream`. Such an instance does not initiate a new connection but instead attaches to the provided stream.
---
# Fetch
URL: https://developers.cloudflare.com/workers/runtime-apis/fetch/
import { TabItem, Tabs } from "~/components";
The [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) provides an interface for asynchronously fetching resources via HTTP requests inside of a Worker.
:::note
Asynchronous tasks such as `fetch` must be executed within a [handler](/workers/runtime-apis/handlers/). If you try to call `fetch()` within [global scope](https://developer.mozilla.org/en-US/docs/Glossary/Global_scope), your Worker will throw an error. Learn more about [the Request context](/workers/runtime-apis/request/#the-request-context).
:::
:::caution[Worker to Worker]
Worker-to-Worker `fetch` requests are possible with [Service bindings](/workers/runtime-apis/bindings/service-bindings/) or by enabling the [`global_fetch_strictly_public` compatibility flag](/workers/configuration/compatibility-flags/#global-fetch-strictly-public).
:::
## Syntax
fetch(resource, options optional)
: Promise`getAttribute(namestring)
: string | null
- Returns the value for a given attribute name on the element, or `null` if it is not found.
- hasAttribute(namestring)
: boolean
- Returns a boolean indicating whether an attribute exists on the element.
- setAttribute(namestring, valuestring)
: Element
- Sets an attribute to a provided value, creating the attribute if it does not exist.
- removeAttribute(namestring)
: Element
- Removes the attribute.
- before(contentContent, contentOptionsContentOptionsoptional)
:
Element
- Inserts content before the element.
after(contentContent, contentOptionsContentOptionsoptional)
:
Element
- Inserts content right after the element.
- prepend(contentContent, contentOptionsContentOptionsoptional)
:
Element
- Inserts content right after the start tag of the element.
- append(contentContent, contentOptionsContentOptionsoptional)
:
Element
- Inserts content right before the end tag of the element.
- replace(contentContent, contentOptionsContentOptionsoptional)
:
Element
- Removes the element and inserts content in place of it.
- setInnerContent(contentContent, contentOptionsContentOptionsoptional)
:
Element
- Replaces content of the element.
- remove()
: Element
- Removes the element with all its content.
- removeAndKeepContent()
: Element
- Removes the start tag and end tag of the element but keeps its inner content intact.
- `onEndTag(handlerFunctionbefore(contentContent, contentOptionsContentOptionsoptional)
:
EndTag
- Inserts content right before the end tag.
- after(contentContent, contentOptionsContentOptionsoptional)
:
EndTag
- Inserts content right after the end tag.
remove()
: EndTag
- Removes the element with all its content.
### Text chunks
Since Cloudflare performs zero-copy streaming parsing, text chunks are not the same thing as text nodes in the lexical tree. A lexical tree text node can be represented by multiple chunks, as they arrive over the wire from the origin.
Consider the following markup: `before(contentContent, contentOptionsContentOptionsoptional)
:
Element
- Inserts content before the element.
after(contentContent, contentOptionsContentOptionsoptional)
:
Element
- Inserts content right after the element.
- replace(contentContent, contentOptionsContentOptionsoptional)
:
Element
- Removes the element and inserts content in place of it.
- remove()
: Element
- Removes the element with all its content.
### Comments
The `comments` function on an element handler allows developers to query and manipulate HTML comment tags.
```js
class ElementHandler {
comments(comment) {
// An incoming comment element, such as
}
}
```
#### Properties
- `comment.removed` boolean
- Indicates whether the element has been removed or replaced by one of the previous handlers.
- `comment.text` string
- The text of the comment. This property can be assigned different values, to modify comment’s text.
#### Methods
- before(contentContent, contentOptionsContentOptionsoptional)
:
Element
- Inserts content before the element.
after(contentContent, contentOptionsContentOptionsoptional)
:
Element
- Inserts content right after the element.
- replace(contentContent, contentOptionsContentOptionsoptional)
:
Element
- Removes the element and inserts content in place of it.
- remove()
: Element
- Removes the element with all its content.
### Doctype
The `doctype` function on a document handler allows developers to query a document’s [doctype](https://developer.mozilla.org/en-US/docs/Glossary/Doctype).
```js
class DocumentHandler {
doctype(doctype) {
// An incoming doctype element, such as
//
}
}
```
#### Properties
- `doctype.name` string | null read-only
- The doctype name.
- `doctype.publicId` string | null read-only
- The quoted string in the doctype after the PUBLIC atom.
- `doctype.systemId` string | null read-only
- The quoted string in the doctype after the SYSTEM atom or immediately after the `publicId`.
### End
The `end` function on a document handler allows developers to append content to the end of a document.
```js
class DocumentHandler {
end(end) {
// The end of the document
}
}
```
#### Methods
- append(contentContent, contentOptionsContentOptionsoptional)
:
DocumentEnd
- Inserts content after the end of the document.